@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,405 @@
1
+ import { AxiosInstance } from 'axios';
2
+ import { print } from 'graphql';
3
+ import gql from 'graphql-tag';
4
+ import { AbstractService } from '../base/AbstractService';
5
+ import { Logger } from '../../Logger';
6
+ import InspiraShopifyError from '../errors/InspiraShopifyError';
7
+ import ErrorHelper from '../base/ErrorHelper';
8
+
9
+ export class ShopifyProductMetafieldsService extends AbstractService {
10
+
11
+ constructor(private axiosInstance: AxiosInstance) { super(); }
12
+
13
+ /**
14
+ * Gets All metafields of a product (Max of 200)
15
+ *
16
+ * @param {number} productId
17
+ * @returns Promise
18
+ */
19
+ public getMetafields = async (productId: number): Promise<IMetafield[]> => {
20
+ try {
21
+ Logger.info(`ShopifyProductService - getMetafields -> Getting metafields for product id -> ${productId}`);
22
+ const rawQuery = ` {
23
+ product(id: "gid://shopify/Product/${productId}") {
24
+ id
25
+ metafields(first: 200) {
26
+ nodes {
27
+ key
28
+ namespace
29
+ value
30
+ id
31
+ description
32
+ type
33
+ }
34
+ }
35
+ }
36
+ }`;
37
+ const query = gql`${rawQuery}`;
38
+ Logger.info(`ShopifyProductService - getMetafields -> rawQuery ${rawQuery}`);
39
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: 20 });
40
+ if (response.data.data.product) {
41
+ if (response.data.data.product.metafields) {
42
+ if (response.data.data.product.metafields.nodes && response.data.data.product.metafields.nodes.length > 0) {
43
+ const metafieldsNodes: IMetafieldGraphQL[] = response.data.data.product.metafields.nodes;
44
+ const metafields: IMetafield[] = metafieldsNodes.map(m => ({ id: this.getIdFromGraphId(m.id), description: m.description, key: m.key, value: m.value, namespace: m.namespace, type: m.type }));
45
+ return metafields;
46
+ } else {
47
+ return [];
48
+ }
49
+ } else {
50
+ Logger.error(`ShopifyProductService - getMetafields -> not the expected response. Data ${JSON.stringify(response.data.data)}. Error is ${JSON.stringify(response.data.errors)}`);
51
+ return [];
52
+ }
53
+ } else {
54
+ return [];
55
+ }
56
+ } catch (error) { this.logErrorAndThrow(error); }
57
+ };
58
+
59
+ /**
60
+ *
61
+ * Adds or updates a metafield.
62
+ * Metafield type, key, namespace and value must be set
63
+ *
64
+ * @param {number} id - product ID
65
+ * @param {IMetafield[]} metafields to be saved
66
+ * @returns Promise
67
+ */
68
+ public saveOrUpdateMetafields = async (id: number, metafields: IMetafield[]): Promise<IMetafield[]> => {
69
+ try {
70
+ Logger.info(`ShopifyProductService - saveOrUpdateMetafields -> for product id -> ${id}`);
71
+
72
+ const metafieldsChunks = this.sliceIntoChunks(metafields, 20);
73
+
74
+ if (metafields?.length) {
75
+ for (const [index, metafiledsChunk] of metafieldsChunks.entries()) {
76
+ Logger.info(`ShopifyProductService - saveOrUpdateMetafields -> processing chunck ${index}`);
77
+ const metafieldsToPost: IMetafieldCreate_GraphQL[] = metafiledsChunk.map(m => ({ ownerId: this.getGraphProductIdFromId(id), value: m.value, key: m.key, type: m.type, namespace: m.namespace }));
78
+ const rawQuery = `mutation MetafieldsSet($metafields: [MetafieldsSetInput!]!) {
79
+ metafieldsSet(metafields: $metafields) {
80
+ metafields {
81
+ id
82
+ key
83
+ namespace
84
+ value
85
+ type
86
+ }
87
+ userErrors {
88
+ field
89
+ message
90
+ code
91
+ }
92
+ }
93
+ }`;
94
+ Logger.info(`ShopifyProductService - saveOrUpdateMetafields -> ${rawQuery}`);
95
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(gql`${rawQuery}`), variables: { metafields: metafieldsToPost } }, { query_cost: 10 * metafiledsChunk.length });
96
+ Logger.info(`ShopifyProductService - saveOrUpdateMetafields -> ${JSON.stringify(response.data)}`);
97
+ if (response.data?.data?.metafieldsSet?.metafields?.length > 0) {
98
+ return response.data?.data?.metafieldsSet?.metafields.map((p: IMetafieldGraphQL) => ({ id: this.getIdFromGraphId(p.id), key: p.key, value: p.value, namespace: p.namespace, type: p.type }));
99
+ } else {
100
+ if (response?.data?.data?.metafieldsSet?.userErrors) {
101
+ Logger.error(`ShopifyProductService - saveOrUpdateMetafields -> ${JSON.stringify(response?.data?.data?.metafieldsSet?.userErrors)}`);
102
+ throw new InspiraShopifyError({ message: JSON.stringify(response.data?.data?.metafieldsSet?.userErrors) });
103
+ } else {
104
+ return [];
105
+ }
106
+ }
107
+ }
108
+ } else {
109
+ Logger.info(`ShopifyProductService - saveOrUpdateMetafields -> for product id -> ${id} - no metafields provided`);
110
+ }
111
+
112
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); throw new InspiraShopifyError(error); }
113
+ };
114
+
115
+ /**
116
+ * Add a metafield to a product for multiple metafield product updates.
117
+ *
118
+ * @param {IMetafieldOfProduct[]} metafieldForProducts
119
+ * @returns Promise
120
+ */
121
+ public addMetafieldForProducts = async (metafieldForProducts: IMetafieldOfProduct[]): Promise<number[]> => {
122
+ return new Promise<number[]>(async (resolve, reject) => {
123
+ try {
124
+ const productIds: number[] = [];
125
+ const metafieldForProductschunks: IMetafieldOfProduct[][] = this.sliceIntoChunks(metafieldForProducts, 5);
126
+ for (const metafieldForProductsChunk of metafieldForProductschunks) {
127
+
128
+ const rawQuery: string = `{
129
+ ${metafieldForProductsChunk.map((mForProduct) => (`
130
+ product${mForProduct.product_id}: product( id: "${this.getGraphProductIdFromId(mForProduct.product_id)}") {
131
+ metafield1: metafield(
132
+ namespace: "${mForProduct.namespace.trim()}"
133
+ key: "${mForProduct.key.trim()}"
134
+ ) {
135
+ value
136
+ key
137
+ namespace
138
+ id
139
+ }
140
+ }
141
+ `)).join('')} }`;
142
+ Logger.info('ExistingMetafields - raw query ', rawQuery);
143
+ const responseOfExistingMetafields = await this.axiosInstance.post('/graphql.json', { query: print(gql`${rawQuery}`) }, { query_cost: 30 * metafieldForProductsChunk.length });
144
+ const existingMetafieldForProduct: IMetafieldOfProduct[] = [];
145
+ Logger.info('responseOfExistingMetafields - ', JSON.stringify(responseOfExistingMetafields.data));
146
+ if (responseOfExistingMetafields.data.errors && responseOfExistingMetafields.data.errors.length > 0) {
147
+ Logger.error('responseOfExistingMetafields with errors - ', JSON.stringify(responseOfExistingMetafields.data));
148
+ throw new Error(`${JSON.stringify(responseOfExistingMetafields.data.errors)} - responseOfExistingMetafields - ${JSON.stringify(rawQuery)}`);
149
+ } else if (responseOfExistingMetafields.data && responseOfExistingMetafields.data.data) {
150
+ for (const mForProduct of metafieldForProductsChunk) {
151
+ const product = responseOfExistingMetafields.data.data[`product${mForProduct.product_id}`];
152
+ if (product && product.metafield1) {
153
+ existingMetafieldForProduct.push({
154
+ key: product.metafield1.key,
155
+ value: product.metafield1.value,
156
+ namespace: product.metafield1.namespace,
157
+ id: this.getIdFromGraphId(product.metafield1.id),
158
+ product_id: mForProduct.product_id
159
+ });
160
+ }
161
+ }
162
+ if (existingMetafieldForProduct && existingMetafieldForProduct.length > 0) {
163
+ for (const metaForproduct of metafieldForProductsChunk) {
164
+ if (metaForproduct.product_id) {
165
+ const existingMetafield = existingMetafieldForProduct.find((em) => em.product_id === metaForproduct.product_id);
166
+ if (existingMetafield) {
167
+ metaForproduct.id = existingMetafield.id;
168
+ }
169
+ }
170
+ }
171
+ }
172
+ const rawQuery = `mutation {
173
+ ${metafieldForProductsChunk.map((mForProduct) => (`
174
+ product${mForProduct.product_id}: productUpdate(input: { id: "${this.getGraphProductIdFromId(mForProduct.product_id)}",
175
+ metafields: [{ namespace: "${mForProduct.namespace.trim()}", key: "${mForProduct.key.trim()}", value: "${this.escapeQuotes(mForProduct.value)}", type: "${this.getMetafieldType(mForProduct.type, mForProduct.value)}",
176
+ description: "${this.escapeQuotes(mForProduct.description)}" ${mForProduct.id ? `, id: "${this.getGraphMetafieldIdFromId(mForProduct.id)}"` : ''}}
177
+ ]
178
+ })
179
+ { product {
180
+ id
181
+ }
182
+ }`)).join('')} }`;
183
+ Logger.info('ExistingMetafields - raw query - update ', rawQuery);
184
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(gql`${rawQuery}`) }, { query_cost: 40 * metafieldForProductsChunk.length });
185
+ Logger.info('response updated Metafields - ', JSON.stringify(response.data));
186
+ if (response.data.errors && response.data.errors.length > 0) {
187
+ Logger.error('response updated Metafields with errors - ', JSON.stringify(response.data));
188
+ throw new Error(`${JSON.stringify(response.data.errors)} - response updated Metafields - ${JSON.stringify(rawQuery)}`);
189
+ } else if (response.data && response.data.data) {
190
+ for (const key in response.data.data) {
191
+ const product = response.data.data[key];
192
+ productIds.push(this.getIdFromGraphId(product.product.id));
193
+ }
194
+ }
195
+ }
196
+ }
197
+ resolve(productIds);
198
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
199
+ });
200
+ };
201
+
202
+ /**
203
+ * Adds Multiple metafields in a product. If the metafield id is provided then it will also update the metafield.
204
+ *
205
+ * @param {number} id - product ID
206
+ * @param {IMetafield[]} metafields to be saved
207
+ * @param {boolean} mergeMetafields when merge Metafields is set to true. It will try to merge metafields even though they do not have a Id setup.
208
+ * @param {boolean} deleteMissingMergedMetafields only affects when merge Metafields is set to true. It will remove existing metafields not been able to get merged.
209
+ * @returns Promise
210
+ */
211
+ public addMetafieldsForAProduct = async (id: number, metafields: IMetafield[], mergeMetafields: boolean, deleteMissingMergedMetafields: boolean): Promise<IMetafield[]> => {
212
+ return new Promise<IMetafield[]>(async (resolve, reject) => {
213
+ try {
214
+ Logger.info(`Add ${metafields.length} Metafields to product: ${id}`);
215
+
216
+ const productMetafieldsInput = { id: this.getGraphProductIdFromId(id), metafields: [] };
217
+ let productExistingMetafields: IMetafield[] = [];
218
+
219
+ if (mergeMetafields) { productExistingMetafields = await this.getMetafields(id); }
220
+
221
+ let metafieldIndex = 0;
222
+
223
+ for (const metafield of metafields) {
224
+ productMetafieldsInput.metafields.push({
225
+ namespace: metafield.namespace, key: metafield.key, value: `${metafield.value}`,
226
+ type: this.getMetafieldType(metafield.type, metafield.value), description: metafield.description
227
+ });
228
+ if (metafield.id) {
229
+ productMetafieldsInput.metafields[metafieldIndex].id = this.getGraphMetafieldIdFromId(metafield.id);
230
+ } else {
231
+ const existingMetafield = productExistingMetafields.find((m) => m.key === metafield.key && m.namespace === metafield.namespace);
232
+ if (existingMetafield) {
233
+ productMetafieldsInput.metafields[metafieldIndex].id = this.getGraphMetafieldIdFromId(existingMetafield.id);
234
+ }
235
+ }
236
+ metafieldIndex++;
237
+ }
238
+
239
+ if (deleteMissingMergedMetafields && productExistingMetafields && productExistingMetafields.length > 0) {
240
+ Logger.info(`Finding metafields to be deleted from product ${id}. Metafields ${JSON.stringify(productExistingMetafields)}`);
241
+ for (const met of productExistingMetafields) {
242
+ const mergedMet = productMetafieldsInput.metafields.find((m) => m.id === this.getGraphMetafieldIdFromId(met.id));
243
+ if (!mergedMet) {
244
+ try {
245
+ Logger.info(`Deleteing metafield ${met.id} from product ${id}`);
246
+ await this.deleteMetafieldsByIds(id, [met.id]);
247
+ } catch (e) {
248
+ Logger.error('We could not delete the metafield. Keep going ...', e.message);
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ const rawQuery = `mutation productUpdate($input: ProductInput!) {
255
+ productUpdate(input: $input)
256
+ { product {
257
+ metafields(first: 100) {
258
+ edges {
259
+ node {
260
+ id
261
+ namespace
262
+ key
263
+ value
264
+ description
265
+ type
266
+ }
267
+ }
268
+ }
269
+ }
270
+ }
271
+ }`;
272
+
273
+ Logger.info('addMetafieldsForAProduct query:');
274
+ Logger.info(rawQuery);
275
+ Logger.info('addMetafieldsForAProduct variable:');
276
+ Logger.info(productMetafieldsInput);
277
+ const query = gql`${rawQuery}`;
278
+
279
+ if (productMetafieldsInput.metafields.length > 0) {
280
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(query), variables: { input: productMetafieldsInput } }, { query_cost: 112 });
281
+ if (response && response.data && response.data.data && response.data.data.productUpdate) {
282
+ try {
283
+ const metafields: IMetafield[] = [];
284
+ for (const edge of response.data.data.productUpdate.product.metafields.edges) {
285
+ metafields.push({
286
+ id: this.getIdFromGraphId(edge.node.id), value: edge.node.value, type: this.getMetafieldType(edge.node.type, edge.node.value),
287
+ description: edge.node.description, namespace: edge.node.namespace, key: edge.node.key
288
+ });
289
+ }
290
+ resolve(metafields);
291
+ } catch (e) {
292
+ resolve([]);
293
+ }
294
+ } else {
295
+ resolve([]);
296
+ }
297
+ } else {
298
+ resolve([]);
299
+ }
300
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
301
+ });
302
+ };
303
+
304
+ /**
305
+ * Deletes a metafields by namespace for a product.
306
+ *
307
+ * @param {number} productId
308
+ * @param {string} namespace
309
+ * @returns Promise number of deleted metafields
310
+ */
311
+ public deleteMetafieldsByNamespace = async (productId: number, namespace: string): Promise<number> => {
312
+ try {
313
+ Logger.info(`Deleting metafield by namespace -> ${namespace} for product ${productId}`);
314
+ const metafields = await this.getMetafields(productId);
315
+ const metafieldsToDelete = metafields.filter((m) => m.namespace === namespace);
316
+
317
+ const query = gql`mutation metafieldsDelete($metafields: [MetafieldIdentifierInput!]!) {
318
+ metafieldsDelete(metafields: $metafields) {
319
+ deletedMetafields {
320
+ key
321
+ ownerId
322
+ }
323
+ userErrors {
324
+ field
325
+ message
326
+ }
327
+ }
328
+ }`;
329
+ const metafiedlsToDeleteVar = metafieldsToDelete.map((m) => ({ ownerId: this.getGraphProductIdFromId(productId), namespace: namespace, key: m.key }));
330
+ const metafieldsDeleteResponse = await this.axiosInstance.post('/graphql.json', { query: print(query), variables: { metafields: metafiedlsToDeleteVar } }, { query_cost: 10 * metafieldsToDelete.length });
331
+ console.log(JSON.stringify(metafieldsDeleteResponse.data));
332
+ if (metafieldsDeleteResponse.data?.data?.metafieldsDelete?.deletedMetafields) {
333
+ let numDeleted = 0;
334
+ // eslint-disable-next-line no-unsafe-optional-chaining
335
+ for (const metaId of metafieldsDeleteResponse.data?.data?.metafieldsDelete?.deletedMetafields) {
336
+ if (metaId && metaId.ownerId) {
337
+ numDeleted += 1;
338
+ }
339
+ }
340
+ return numDeleted;
341
+ } else {
342
+ Logger.error(`deleteMetafieldsByNamespace - ${JSON.stringify(metafieldsDeleteResponse.data?.data?.metafieldsDelete?.userErrors)}`);
343
+ return 0;
344
+ }
345
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); throw new InspiraShopifyError(error); }
346
+ };
347
+
348
+ /**
349
+ * Gets a metafield matching a product namespace and key.
350
+ *
351
+ * @param {number} productId
352
+ * @param {string} namespace
353
+ * @param {string} key
354
+ * @returns Promise
355
+ */
356
+ public getMetafield = async (productId: number, namespace: string, key: string): Promise<IMetafield> => {
357
+ return new Promise<IMetafield>(async (resolve, reject) => {
358
+ try {
359
+ Logger.info(`Getting metafield for product id -> ${productId} and namespace ${namespace} and key ${key}`);
360
+ const metafields = await this.getMetafields(productId);
361
+ let foundMetafield: IMetafield = null;
362
+ if (metafields && metafields.length > 0) {
363
+ foundMetafield = metafields.find(m => m.namespace === namespace && m.key === key);
364
+ }
365
+ resolve(foundMetafield);
366
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
367
+ });
368
+ };
369
+
370
+ /**
371
+ * Deletes a metafields by ID for a product.
372
+ *
373
+ * @param {number} productId
374
+ * @param {number} metafieldIds (max of 50 metafileds)
375
+ * @returns Promise return num of deleted metafiedls
376
+ */
377
+ public deleteMetafieldsByIds = async (productId: number, metafieldIds: number[]): Promise<number> => {
378
+ try {
379
+ Logger.info(`Deleting metafield ids -> ${metafieldIds} for product ${productId}`);
380
+ const query = gql`mutation {
381
+ ${metafieldIds.map((id) => (`
382
+ metafield${id}: metafieldDelete(input: { id: "${this.getGraphMetafieldIdFromId(id)}" }) {
383
+ deletedId
384
+ userErrors {
385
+ field
386
+ message
387
+ }
388
+ }`)).join('')}
389
+ }`;
390
+ const metafieldsDeleteResponse = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: 10 * metafieldIds.length });
391
+ if (metafieldsDeleteResponse.data?.data) {
392
+ let numDeleted = 0;
393
+ for (const metaId of metafieldIds) {
394
+ if (metafieldsDeleteResponse.data?.data[`metafield${metaId}`].deletedId) {
395
+ numDeleted += 1;
396
+ }
397
+ }
398
+ return numDeleted;
399
+ } else {
400
+ return 0;
401
+ }
402
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); throw new InspiraShopifyError(error); }
403
+ };
404
+
405
+ }
@@ -0,0 +1,112 @@
1
+ import { AxiosInstance } from 'axios';
2
+ import { print } from 'graphql';
3
+ import gql from 'graphql-tag';
4
+ import { Logger } from '../../Logger';
5
+ import { AbstractService } from '../base/AbstractService';
6
+ import InspiraShopifyError from '../errors/InspiraShopifyError';
7
+
8
+ export class ShopifyProductPublicationsService extends AbstractService {
9
+
10
+ constructor(private axiosInstance: AxiosInstance) { super(); }
11
+
12
+ /**
13
+ * Returns a max of 15 publications
14
+ *
15
+ * @returns {IProductPublications[]} list of publications in store.
16
+ */
17
+ public getPublications = async (): Promise<IProductPublications[]> => {
18
+ try {
19
+ Logger.info('Get product publications');
20
+ const query = gql`{
21
+ publications(first: 15) {
22
+ edges {
23
+ node {
24
+ id
25
+ name
26
+ autoPublish
27
+ supportsFuturePublishing
28
+ }
29
+ }
30
+ }
31
+ }`;
32
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: 15 });
33
+ if (response && response.data && response.data.data && response.data.data.publications) {
34
+ const productPublicationsGraph: { node: IProductPublications }[] = response.data.data.publications.edges;
35
+ const productPublications: IProductPublications[] = productPublicationsGraph.map((p) => p.node);
36
+ return productPublications;
37
+ } else {
38
+ throw new InspiraShopifyError({ message: JSON.stringify(response.data.errors) });
39
+ }
40
+ } catch (error) { this.logErrorAndThrow(error); }
41
+ };
42
+
43
+ /**
44
+ * Publishes product to all publication channels.
45
+ *
46
+ * @param {number} productId
47
+ * @returns {IProductPublishResult}
48
+ */
49
+ public publish = async (productId: number): Promise<IProductPublishResult> => {
50
+ try {
51
+ Logger.info(`Publish product with id ${productId} to all publications`);
52
+ const publishQuery = gql`mutation productPublish($input: ProductPublishInput!) {
53
+ productPublish(input: $input) {
54
+ product {
55
+ id
56
+ }
57
+ productPublications {
58
+ isPublished
59
+ channel {
60
+ name
61
+ }
62
+ }
63
+ userErrors {
64
+ field
65
+ message
66
+ }
67
+ }
68
+ }`;
69
+ const publications = await this.getPublications();
70
+ const inputParams = { id: this.getGraphProductIdFromId(productId), productPublications: publications.map(p => ({ publicationId: p.id})) };
71
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(publishQuery), variables: { input: inputParams } }, { query_cost: 10 });
72
+ if (response && response.data && response.data.data && response.data.data.productPublish) {
73
+ const productPublications: { isPublished: boolean; channel: { name: string;} } []= response.data.data.productPublish.productPublications;
74
+
75
+ return { id: productId, productPublications: productPublications.filter(pp => pp.isPublished).map(pp => pp.channel) };
76
+ } else {
77
+ throw new InspiraShopifyError({ message: JSON.stringify(response.data.errors) });
78
+ }
79
+ } catch (error) { this.logErrorAndThrow(error); }
80
+ };
81
+
82
+ /**
83
+ * Unpublishes product to all publication channels.
84
+ *
85
+ * @param {number} productId
86
+ * @returns {IProductUnpublishResult}
87
+ */
88
+ public unPublish = async (productId: number): Promise<IProductUnpublishResult> => {
89
+ try {
90
+ Logger.info(`Unpublish product with id ${productId} to all publications`);
91
+ const publishQuery = gql`mutation productUnpublish($input: ProductUnpublishInput!) {
92
+ productUnpublish(input: $input) {
93
+ product {
94
+ id
95
+ }
96
+ userErrors {
97
+ field
98
+ message
99
+ }
100
+ }
101
+ }`;
102
+ const publications = await this.getPublications();
103
+ const inputParams = { id: this.getGraphProductIdFromId(productId), productPublications: publications.map(p => ({ publicationId: p.id})) };
104
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(publishQuery), variables: { input: inputParams } }, { query_cost: 10 });
105
+ if (response && response.data && response.data.data && response.data.data.productUnpublish) {
106
+ return { id: productId };
107
+ } else {
108
+ throw new InspiraShopifyError({ message: JSON.stringify(response.data.errors) });
109
+ }
110
+ } catch (error) { this.logErrorAndThrow(error); }
111
+ };
112
+ }