@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,584 @@
1
+ import { print } from 'graphql';
2
+ import gql from 'graphql-tag';
3
+ import { AxiosInstance } from 'axios';
4
+ import { AbstractService } from '../base/AbstractService';
5
+ import { Logger } from '../../Logger';
6
+ import InspiraShopifyError from '../errors/InspiraShopifyError';
7
+ import { ShopifyProductServiceHelper } from '../helpers/ShopifyProductServiceHelper';
8
+
9
+ export type ConfirmationUpdate <
10
+ T extends string,
11
+ U extends string,
12
+ > =
13
+ | {
14
+ [K in T]: true;
15
+ } & {
16
+ [K in U]: boolean;
17
+ }
18
+ | {
19
+ [K in T]: false;
20
+ } & {
21
+ [K in U]?: boolean;
22
+ }
23
+ export class ShopifyVariantService extends AbstractService {
24
+
25
+ constructor(private axiosInstance: AxiosInstance, private shopifyProductServiceHelper: ShopifyProductServiceHelper) {
26
+ super();
27
+ }
28
+
29
+ /**
30
+ *
31
+ * @param {number} productId where to create the variant.
32
+ * @param {IVariant} variant to be created
33
+ * @param {string} imageSrc to add to the variant.
34
+ * @param {string[]} productOptionNames option names. If variant has option values this is required.
35
+ * @param locationId where to add stock to.
36
+ * @returns {IVariant} variant created or null if error.
37
+ */
38
+ public create = async (productId: number, variant: IVariant, imageSrc: string, productOptionNames: string[], locationId: number, retrieveInventory: boolean, removeStandaloneVariant: boolean): Promise<IVariant> => {
39
+ try {
40
+ Logger.info(`ShopifyVariantService - create -> variant for product ${productId} --- variant ${JSON.stringify(variant)}`);
41
+ const variantToCreate: ShopifygraphQl.IVariantToPost = this.shopifyProductServiceHelper.getVariantToPost(variant, { imageSrc: imageSrc }, productOptionNames, locationId);
42
+ const variantsCreateQuery = ShopifyProductServiceHelper.graphQLQuery.getVariantsBulkCreateMutation(imageSrc ? true : false, retrieveInventory, removeStandaloneVariant);
43
+ Logger.info(`ShopifyVariantService - create -> Product Variant to create .... ${JSON.stringify(variantToCreate)}`);
44
+ Logger.info(`ShopifyVariantService - create -> Product Variant to create query .... ${JSON.stringify(variantsCreateQuery)}`);
45
+ const variables: { productId: string; variants: ShopifygraphQl.IVariantToPost[], media?: { mediaContentType: 'IMAGE', originalSource: string; }[]; } = { productId: this.getGraphProductIdFromId(productId), variants: [variantToCreate] };
46
+ if (imageSrc) {
47
+ variables.media = [{ originalSource: imageSrc.trim().replace(/ /g, '%20'), mediaContentType: 'IMAGE' }];
48
+ }
49
+ const variantsResponse = await this.axiosInstance.post('/graphql.json', { query: print(gql`${variantsCreateQuery}`), variables: variables }, { query_cost: 50 });
50
+ let createdVariant = null;
51
+ if (variantsResponse && variantsResponse.data && variantsResponse.data.data && variantsResponse.data.data.productVariantsBulkCreate && variantsResponse.data.data.productVariantsBulkCreate.productVariants) {
52
+ for (const graphVariant of variantsResponse.data.data.productVariantsBulkCreate.productVariants) {
53
+ createdVariant = this.shopifyProductServiceHelper.getVariant(graphVariant);
54
+ }
55
+ } else {
56
+ Logger.error(`ShopifyVariantService - create -> Product variant was not created. ${productId}`);
57
+ }
58
+ return createdVariant;
59
+ } catch (error) { this.logErrorAndThrow(error); }
60
+ };
61
+
62
+ /**
63
+ * To update inventory prodvide locationID.
64
+ * To update option value provide option Names.
65
+ * When imageSrc is set to null it is not updated.
66
+ *
67
+ * @param {IVariant} variant
68
+ * @param {number} locationID
69
+ * @param {string[]} productOptionNames
70
+ * @param {number} variantId
71
+ * @param {number} productId
72
+ * @param {imageSrc?: string; imageId?: number;} img
73
+ *
74
+ * @returns
75
+ */
76
+ public update = async (variant: IVariant, productOptionNames: string[], locationID: number, variantId: number, productId: number, img: { imageSrc?: string; imageId?: number; }, retrieveInventory: boolean, taxableSetting: ConfirmationUpdate<'updateTaxable', 'taxable'>, trackInventorySetting: ConfirmationUpdate<'updateTrackInventory', 'trackInventory'>): Promise<IVariant> => {
77
+ try {
78
+ Logger.info(`ShopifyVariantService - update variant with id ${variantId} - variant: ${JSON.stringify(variant)}`);
79
+ if(trackInventorySetting.updateTrackInventory) {
80
+ variant.inventory_management = trackInventorySetting.trackInventory ? 'SOMEONE' : 'NOT_MANGED';
81
+ }
82
+ const variantToUpdate: ShopifygraphQl.IVariantToPost = this.shopifyProductServiceHelper.getVariantToUpdate(variant, { imageSrc: img?.imageSrc, imageId: img?.imageId }, productOptionNames, taxableSetting);
83
+ variantToUpdate.id = this.getGraphProductVariantIdFromId(variantId);
84
+ const variantsCreateQuery = retrieveInventory ? ShopifyProductServiceHelper.graphQLQuery.productVariantsBulkUpdate : ShopifyProductServiceHelper.graphQLQuery.productVariantsBulkUpdateWithoutInventory;
85
+ Logger.info(`ShopifyVariantService - update -> Product Variant to update .... ${JSON.stringify(variantToUpdate)}`);
86
+ Logger.info(`ShopifyVariantService - update -> Product Variant to update query .... ${JSON.stringify(variantsCreateQuery)}`);
87
+ const variantsResponse = await this.axiosInstance.post('/graphql.json', { query: print(gql`${variantsCreateQuery}`), variables: { productId: this.getGraphProductIdFromId(productId), variants: [variantToUpdate] } }, { query_cost: 50 });
88
+ let updatedVariant: IVariant = null;
89
+ if (variantsResponse && variantsResponse.data && variantsResponse.data.data && variantsResponse.data.data.productVariantsBulkUpdate && variantsResponse.data.data.productVariantsBulkUpdate.productVariants) {
90
+ const variants: ShopifygraphQl.IGenericVariantResponse[] = variantsResponse.data.data.productVariantsBulkUpdate.productVariants;
91
+ for (const graphVariant of variants) {
92
+ if (variantId === this.getIdFromGraphId(graphVariant.id)) {
93
+ updatedVariant = this.shopifyProductServiceHelper.getVariant(graphVariant);
94
+ if (locationID && variant.inventory_quantity) {
95
+ Logger.info(`ShopifyVariantService - update -> Inventory adjust for product ${productId} and variant ${variantId}`);
96
+ await this.setInventory(locationID, updatedVariant.inventory_item_id, variant.inventory_quantity);
97
+ updatedVariant.inventory_quantity = variant.inventory_quantity;
98
+ }
99
+ break;
100
+ }
101
+ }
102
+ } else {
103
+ Logger.error(`ShopifyVariantService - update -> Product variant was not updated. ${productId} - ${JSON.stringify(variantsResponse.data.data)}`);
104
+ }
105
+ return updatedVariant;
106
+ } catch (error) { this.logErrorAndThrow(error); }
107
+ };
108
+
109
+ /**
110
+ * To update option value provide option Names.
111
+ * To update images, provide image data mapped to variant ID
112
+ * Provide Location to update inventory. Location ID must be mapped to variant ID
113
+ *
114
+ * @param {number} productId
115
+ * @param {IVariant[]} variants
116
+ * @param {{ productOptionNames: string[], imgs?: { variantId: number, img?: { imageSrc?: string; imageId?: number }}[] }} updateProperty
117
+ * @param {boolean} retrieveInventory
118
+ *
119
+ * @returns
120
+ */
121
+ public bulkUpdate = async (productId: number, variants: IVariant[], updateProperty: {
122
+ productOptionNames: string[],
123
+ locationIDs: { variantId: number, locationID: number }[],
124
+ imgs?: { variantId: number, img?: { imageSrc?: string; imageId?: number } }[]
125
+ }, retrieveInventory: boolean): Promise<IVariant[]> => {
126
+ try {
127
+ if (!variants.every(el => el.id)) throw new Error('Some variants does not have ID associated with them');
128
+
129
+ Logger.info(`ShopifyVariantService - updating variants - ${JSON.stringify(variants)} - for product ID ${productId}`);
130
+
131
+ const variantsToUpdate: ShopifygraphQl.IVariantToPost[] = [];
132
+ variants.forEach(v => {
133
+ const i_mg = updateProperty.imgs?.find(i => i.variantId === v.id).img;
134
+ const v_prep = this.shopifyProductServiceHelper.getVariantToUpdate(
135
+ v,
136
+ { imageSrc: i_mg?.imageSrc, imageId: i_mg?.imageId },
137
+ updateProperty.productOptionNames,
138
+ { updateTaxable: true, taxable: v.taxable }
139
+ );
140
+ v_prep.id = this.getGraphProductVariantIdFromId(v.id);
141
+ variantsToUpdate.push(v_prep);
142
+ });
143
+
144
+ const variantBulkUpdateQuery = retrieveInventory ? ShopifyProductServiceHelper.graphQLQuery.productVariantsBulkUpdate : ShopifyProductServiceHelper.graphQLQuery.productVariantsBulkUpdateWithoutInventory;
145
+ Logger.info(`ShopifyVariantService - updating variants - Prepared Variants : ${JSON.stringify(variantsToUpdate)}`);
146
+ Logger.info(`ShopifyVariantService - updating variants - Bulk update variants query : ${JSON.stringify(variantBulkUpdateQuery)}`);
147
+ const variantsResponse = await this.axiosInstance.post('/graphql.json', { query: print(gql`${variantBulkUpdateQuery}`), variables: { productId: this.getGraphProductIdFromId(productId), variants: variantsToUpdate } });
148
+
149
+ const updatedVariants: IVariant[] = [];
150
+ if (variantsResponse && variantsResponse.data && variantsResponse.data.data && variantsResponse.data.data.productVariantsBulkUpdate && variantsResponse.data.data.productVariantsBulkUpdate.productVariants) {
151
+ const responseVariants: ShopifygraphQl.IGenericVariantResponse[] = variantsResponse.data.data.productVariantsBulkUpdate.productVariants;
152
+ for (const graphVariant of responseVariants) {
153
+ const updatedVariant = this.shopifyProductServiceHelper.getVariant(graphVariant);
154
+ const originalVariant = variants.find(v => v.id === this.getIdFromGraphId(graphVariant.id));
155
+
156
+ if (originalVariant) {
157
+ const location = updateProperty.locationIDs.find(l => l.variantId === updatedVariant.id);
158
+
159
+ if (location.locationID) {
160
+ Logger.info(`ShopifyVariantService - update -> Inventory adjust for product ${productId} and variant ${updatedVariant.id}`);
161
+ await this.setInventory(location.locationID, updatedVariant.inventory_item_id, updatedVariant.inventory_quantity);
162
+ } else {
163
+ Logger.error(`ShopifyVariantService - update -> Location ID not found for variant ${updatedVariant.id}`);
164
+ Logger.error(`ShopifyVariantService - update -> Failed to update inventory for variant ${updatedVariant.id}`);
165
+ }
166
+ updatedVariants.push(updatedVariant);
167
+ break;
168
+ }
169
+ }
170
+ } else {
171
+ Logger.error(`ShopifyVariantService - update -> Product variant was not updated. ${productId} - ${JSON.stringify(variantsResponse.data.data)}`);
172
+ }
173
+
174
+ return updatedVariants;
175
+ } catch (error) { this.logErrorAndThrow(error); }
176
+ };
177
+
178
+ /**
179
+ *
180
+ * @param {number} locationID
181
+ * @param {number} inventoryItemId
182
+ * @param {number} delta
183
+ * @returns
184
+ */
185
+ public adjustInventory = async (locationID: number, inventoryItemId: number, delta: number): Promise<boolean> => {
186
+ try {
187
+ Logger.info(`ShopifyVariantService - adjustInventory - ${delta} in location ${locationID} and inventory item: ${inventoryItemId}`);
188
+
189
+ const inventoryToUpdate: ShopifygraphQl.IVariantInventoryToPut = { reason: 'correction', name: 'available', changes: [{ delta: delta, inventoryItemId: this.getGraphInventoryItemIdFromId(inventoryItemId), locationId: this.getGraphLocationIdFromId(locationID) }] };
190
+
191
+ const variantsUpdateInventory = ShopifyProductServiceHelper.graphQLQuery.variantUpdateInventory;
192
+ Logger.info(`ShopifyVariantService - adjustInventory -> update .... ${JSON.stringify(inventoryToUpdate)}`);
193
+ Logger.info(`ShopifyVariantService - adjustInventory -> query .... ${JSON.stringify(variantsUpdateInventory)}`);
194
+ const variantsResponse = await this.axiosInstance.post('/graphql.json', { query: print(gql`${variantsUpdateInventory}`), variables: { input: inventoryToUpdate } }, { query_cost: 50 });
195
+
196
+ if (variantsResponse && variantsResponse.data && variantsResponse.data.data && variantsResponse.data.data.inventoryAdjustQuantities?.inventoryAdjustmentGroup?.createdAt) {
197
+ Logger.info(`ShopifyVariantService - adjustInventory -> ${JSON.stringify(variantsResponse.data.data.inventoryAdjustQuantities?.inventoryAdjustmentGroup)}`);
198
+ return true;
199
+ } else {
200
+ Logger.error(`We could not adjust inventory Product variant on inventoryItemId ${inventoryItemId} - ${JSON.stringify(variantsResponse.data.data.inventoryAdjustQuantities?.userErrors)}`);
201
+ return false;
202
+ }
203
+ } catch (error) { this.logErrorAndThrow(error); }
204
+ };
205
+
206
+ /**
207
+ *
208
+ * @param {number} locationID
209
+ * @param {number} inventoryItemId
210
+ * @param {number} delta
211
+ * @returns
212
+ */
213
+ public setInventory = async (locationID: number, inventoryItemId: number, quantity: number): Promise<boolean> => {
214
+ try {
215
+ Logger.info(`ShopifyVariantService - setInventory - ${quantity} in location ${locationID} and inventory item: ${inventoryItemId}`);
216
+
217
+ const inventoryToUpdate: ShopifygraphQl.IVariantInventoryToPut = { ignoreCompareQuantity: true, reason: 'correction', name: 'available', quantities: [{ quantity: quantity, inventoryItemId: this.getGraphInventoryItemIdFromId(inventoryItemId), locationId: this.getGraphLocationIdFromId(locationID) }] };
218
+
219
+ const variantsUpdateInventory = ShopifyProductServiceHelper.graphQLQuery.variantSetInventory;
220
+ Logger.info(`ShopifyVariantService - setInventory -> update .... ${JSON.stringify(inventoryToUpdate)}`);
221
+ Logger.info(`ShopifyVariantService - setInventory -> query .... ${JSON.stringify(variantsUpdateInventory)}`);
222
+ const variantsResponse = await this.axiosInstance.post('/graphql.json', { query: print(gql`${variantsUpdateInventory}`), variables: { input: inventoryToUpdate } }, { query_cost: 50 });
223
+
224
+ if (variantsResponse && variantsResponse.data && variantsResponse.data.data && variantsResponse.data.data.inventorySetQuantities?.inventoryAdjustmentGroup?.changes) {
225
+ Logger.info(`ShopifyVariantService - setInventory -> ${JSON.stringify(variantsResponse.data.data.inventorySetQuantities?.inventoryAdjustmentGroup)}`);
226
+ return true;
227
+ } else {
228
+ Logger.error(`We could not set inventory Product variant on inventoryItemId ${inventoryItemId} - ${JSON.stringify(variantsResponse.data.data.inventorySetQuantities?.userErrors)}`);
229
+ return false;
230
+ }
231
+ } catch (error) { this.logErrorAndThrow(error); }
232
+ };
233
+
234
+ public getCount = async (productId: number): Promise<number> => {
235
+
236
+ try {
237
+ Logger.info(`Counting all variants for product ${productId}`);
238
+ const countVariantsQuery = `{ product(id: "${this.getGraphProductIdFromId(productId)}") {
239
+ id
240
+ variants(first: 250) {
241
+ edges {
242
+ node {
243
+ id
244
+ }
245
+ }
246
+ }
247
+ }
248
+ }`;
249
+ const variantsCountResponse = await this.axiosInstance.post('/graphql.json', { query: print(gql`${countVariantsQuery}`) }, { query_cost: 50 });
250
+ if (variantsCountResponse && variantsCountResponse.data && variantsCountResponse.data.data && variantsCountResponse.data.data.product?.variants) {
251
+ return variantsCountResponse.data.data.product?.variants?.edges?.length ? variantsCountResponse.data.data.product.variants.edges.length : 0;
252
+ } else {
253
+ Logger.error(`ShopifyVariantService - getCount -> We could not find variants for ${productId}`);
254
+ }
255
+
256
+ } catch (error) { this.logErrorAndThrow(error); }
257
+ };
258
+
259
+ /**
260
+ *
261
+ * @param {number} variantId
262
+ * @returns {IVariant} variant
263
+ */
264
+ public getById = async (variantId: number): Promise<IVariant> => {
265
+ try {
266
+ Logger.info(`ShopifyVariantService - getById -> Get variant with id ${variantId}`);
267
+ const getVariantQuery = ShopifyProductServiceHelper.graphQLQuery.getProductVariant;
268
+ Logger.info(`ShopifyVariantService - getById -> Product graphQL query going in ${getVariantQuery}`);
269
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(gql`${getVariantQuery}`), variables: { variantId: this.getGraphProductVariantIdFromId(variantId) } }, { query_cost: 10 });
270
+ if (response && response.data && response.data.data && response.data.data.productVariant) {
271
+ const responseVariant: ShopifygraphQl.IGenericVariantResponse = response.data.data.productVariant;
272
+ const variant = this.shopifyProductServiceHelper.getVariant(responseVariant);
273
+ return variant;
274
+ } else {
275
+ Logger.error(`ShopifyVariantService - getById -> ${variantId} not found`);
276
+ return null;
277
+ }
278
+ } catch (error) { this.logErrorAndThrow(error); }
279
+ };
280
+
281
+ public delete = async (productId: number, variantsIds: number[]): Promise<void> => {
282
+ try {
283
+ Logger.info(`ShopifyVariantService - delete -> Delete variant id -> ${variantsIds}`);
284
+ const deleteVariantsMutation = ShopifyProductServiceHelper.graphQLQuery.deleteVariantsMutation;
285
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(gql`${deleteVariantsMutation}`), variables: { productId: this.getGraphProductIdFromId(productId), variantsIds: variantsIds.map(vId => this.getGraphProductVariantIdFromId(vId)) } }, { query_cost: 20 });
286
+ if (response && response.data && response.data.data && response.data.data.productVariantsBulkDelete?.product?.id) {
287
+ Logger.info(`ShopifyVariantService - delete -> Deleted ${variantsIds.length} variants from product ${productId}`);
288
+ } else {
289
+ throw new InspiraShopifyError({ message: `${JSON.stringify(response.data.data.productVariantsBulkDelete?.userErrors)}` });
290
+ }
291
+ } catch (error) { this.logErrorAndThrow(error); }
292
+ };
293
+
294
+ /**
295
+ * Gets All metafields of a product variant
296
+ *
297
+ * @param {number} variantId
298
+ * @returns Promise
299
+ */
300
+ public getMetafields = async (variantId: number): Promise<IMetafield[]> => {
301
+ try {
302
+ Logger.info(`ShopifyVariantService - getMetafields -> Getting metafields for variant id -> ${variantId}`);
303
+ const getMetafieldsQuery = ShopifyProductServiceHelper.graphQLQuery.getProductVariantMetafields;
304
+ const query = gql`${getMetafieldsQuery}`;
305
+ Logger.info(`ShopifyVariantService - getMetafields -> rawQuery ${getMetafieldsQuery}`);
306
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(query), variables: { variantId: this.getGraphProductVariantIdFromId(variantId) } }, { query_cost: 20 });
307
+
308
+ if (response.data.data.productVariant?.metafields?.nodes && response.data.data.productVariant.metafields.nodes.length > 0) {
309
+ const metafieldsNodes: IMetafieldGraphQL[] = response.data.data.productVariant.metafields.nodes;
310
+ 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 }));
311
+ return metafields;
312
+ } else {
313
+ if (response.data.data.productVariant.metafields?.nodes?.length === 0 && !response.data.errors) {
314
+ Logger.info(`ShopifyVariantService - getMetafields -> we haven't found any metafield. Data ${JSON.stringify(response.data.data)}`);
315
+ } else {
316
+ Logger.error(`ShopifyVariantService - getMetafields -> not the expected response. Data ${JSON.stringify(response.data.data)}. Error is ${JSON.stringify(response.data.errors)}`);
317
+ }
318
+ return [];
319
+ }
320
+
321
+ } catch (error) { this.logErrorAndThrow(error); }
322
+ };
323
+
324
+ /**
325
+ *
326
+ * Adds or updates a metafield.
327
+ * Metafield type, key, namespace and value must be set
328
+ *
329
+ * @param {number} id - variant ID
330
+ * @param {IMetafield[]} metafields to be saved
331
+ * @returns Promise
332
+ */
333
+ public saveOrUpdateMetafields = async (id: number, metafields: IMetafield[]): Promise<IMetafield[]> => {
334
+ try {
335
+ Logger.info(`ShopifyVariantService - saveOrUpdateMetafields -> for variant id -> ${id}`);
336
+
337
+ const metafieldsChunks = this.sliceIntoChunks(metafields, 20);
338
+
339
+ if (metafields?.length) {
340
+ for (const [index, metafiledsChunk] of metafieldsChunks.entries()) {
341
+ Logger.info(`ShopifyVariantService - saveOrUpdateMetafields -> processing chunck ${index}`);
342
+ const metafieldsToPost: IMetafieldCreate_GraphQL[] = metafiledsChunk.map(m => ({ ownerId: this.getGraphProductVariantIdFromId(id), value: m.value, key: m.key, type: m.type, namespace: m.namespace }));
343
+ const rawQuery = `mutation MetafieldsSet($metafields: [MetafieldsSetInput!]!) {
344
+ metafieldsSet(metafields: $metafields) {
345
+ metafields {
346
+ id
347
+ key
348
+ namespace
349
+ value
350
+ type
351
+ }
352
+ userErrors {
353
+ field
354
+ message
355
+ code
356
+ }
357
+ }
358
+ }`;
359
+ Logger.info(`ShopifyVariantService - saveOrUpdateMetafields -> ${rawQuery}`);
360
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(gql`${rawQuery}`), variables: { metafields: metafieldsToPost } }, { query_cost: 10 * metafiledsChunk.length });
361
+ Logger.info(`ShopifyVariantService - saveOrUpdateMetafields -> ${JSON.stringify(response.data)}`);
362
+ if (response.data?.data?.metafieldsSet?.metafields?.length > 0) {
363
+ 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 }));
364
+ } else {
365
+ if (response?.data?.data?.metafieldsSet?.userErrors) {
366
+ Logger.error(`ShopifyVariantService - saveOrUpdateMetafields -> ${JSON.stringify(response?.data?.data?.metafieldsSet?.userErrors)}`);
367
+ throw new InspiraShopifyError({ message: JSON.stringify(response.data?.data?.metafieldsSet?.userErrors) });
368
+ } else {
369
+ return [];
370
+ }
371
+ }
372
+ }
373
+ } else {
374
+ Logger.info(`ShopifyVariantService - saveOrUpdateMetafields -> for product id -> ${id} - no metafields provided`);
375
+ }
376
+ } catch (error) { this.logErrorAndThrow(error); }
377
+ };
378
+
379
+
380
+ /**
381
+ * Deletes a metafields by ID for a product.
382
+ *
383
+ * @param {number} metafieldIds (max of 50 metafileds)
384
+ * @returns Promise return num of deleted metafiedls
385
+ */
386
+ public deleteMetafieldsByIds = async (metafieldIds: number[]): Promise<number> => {
387
+ try {
388
+ Logger.info(`ShopifyVariantService - deleteMetafieldsByIds -> Deleting metafield ids -> ${metafieldIds}`);
389
+ const query = gql`mutation {
390
+ ${metafieldIds.map((id) => (`
391
+ metafield${id}: metafieldDelete(input: { id: "${this.getGraphMetafieldIdFromId(id)}" }) {
392
+ deletedId
393
+ userErrors {
394
+ field
395
+ message
396
+ }
397
+ }`)).join('')}
398
+ }`;
399
+ const metafieldsDeleteResponse = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: 10 * metafieldIds.length });
400
+ if (metafieldsDeleteResponse.data?.data) {
401
+ let numDeleted = 0;
402
+ for (const metaId of metafieldIds) {
403
+ if (metafieldsDeleteResponse.data?.data[`metafield${metaId}`].deletedId) {
404
+ numDeleted += 1;
405
+ }
406
+ }
407
+ return numDeleted;
408
+ } else {
409
+ return 0;
410
+ }
411
+ } catch (error) { this.logErrorAndThrow(error); }
412
+ };
413
+
414
+ /**
415
+ * Deletes a metafield by Key for a variant.
416
+ *
417
+ * @param {number} productId
418
+ * @param {number} variantId
419
+ * @param {string} metafieldKey
420
+ * @returns Promise Number of deleted metafields
421
+ */
422
+ public deleteMetafieldByKey = async (productId: number, variantId: number, metafieldKey: string): Promise<number> => {
423
+ try {
424
+ Logger.info(`ShopifyVariantService - deleteMetafieldByKey -> Deleting metafield Key -> ${metafieldKey} for product ${productId} and variant ${variantId}`);
425
+ const metafields = await this.getMetafields(variantId);
426
+ let deleteItems = 0;
427
+ for (const metafield of metafields) {
428
+ if (metafield.key === metafieldKey) {
429
+ await this.deleteMetafieldsByIds([metafield.id]);
430
+ deleteItems++;
431
+ }
432
+ }
433
+ return deleteItems;
434
+ } catch (error) { this.logErrorAndThrow(error); }
435
+ };
436
+
437
+ /**
438
+ * Deletes multiple metafields
439
+ *
440
+ * @param {number[]} productIds
441
+ * @returns Promise
442
+ */
443
+ public deleteMultipleMetafields = async (mertafieldsIds: number[]): Promise<any[]> => {
444
+ try {
445
+ if (mertafieldsIds && mertafieldsIds.length > 0) {
446
+ const errors = [];
447
+ Logger.info(`ShopifyVariantService - deleteMultipleMetafields -> Metafields to delete: ${mertafieldsIds.toString()}`);
448
+ const metafieldIdschunks: number[][] = this.sliceIntoChunks(mertafieldsIds, 50);
449
+ for (const metafieldIdsChunk of metafieldIdschunks) {
450
+ const query = gql`mutation {
451
+ ${metafieldIdsChunk.map((id) => (`
452
+ metafield${id}: metafieldDelete(input: { id: "${this.getGraphMetafieldIdFromId(id)}" }) {
453
+ deletedId
454
+ userErrors {
455
+ field
456
+ message
457
+ }
458
+ }`)).join('')}
459
+ }`;
460
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: 10 * metafieldIdsChunk.length });
461
+
462
+ if (response && response.data && response.data.data) {
463
+ const updatesResponse = response.data.data;
464
+ for (const key in updatesResponse) {
465
+ const err = updatesResponse[key].userErrors;
466
+ if (err && err.length > 0) {
467
+ errors.push(err);
468
+ }
469
+ }
470
+ } else {
471
+ errors.push('No response arrived');
472
+ }
473
+ }
474
+ return errors;
475
+ } else {
476
+ Logger.info('ShopifyVariantService - deleteMultipleMetafields -> Remove metafields without IDs provided');
477
+ return [];
478
+ }
479
+ } catch (error) { this.logErrorAndThrow(error); }
480
+ };
481
+
482
+ /**
483
+ * Deletes a metafield by Key for a variant.
484
+ *
485
+ * @param {number} productId
486
+ * @param {number} variantId
487
+ * @param {string[]} metafieldKeys
488
+ * @returns Promise Number of deleted metafields
489
+ */
490
+ public deleteMetafieldByKeys = async (productId: number, variantId: number, metafieldKeys: string[]): Promise<number> => {
491
+ try {
492
+ Logger.info(`ShopifyVariantService - deleteMetafieldByKeys -> Deleting metafield Keys -> ${JSON.stringify(metafieldKeys)} for product ${productId} and variant ${variantId}`);
493
+ const metafields = await this.getMetafields(variantId);
494
+ let deleteItems = 0;
495
+ const metafieldsTodelete: number[] = [];
496
+ for (const metafield of metafields) {
497
+ if (metafieldKeys.includes(metafield.key)) {
498
+ metafieldsTodelete.push(metafield.id);
499
+ deleteItems++;
500
+ }
501
+ }
502
+ if (metafieldsTodelete.length > 0) {
503
+ this.deleteMultipleMetafields(metafieldsTodelete);
504
+ }
505
+ return deleteItems;
506
+ } catch (error) { this.logErrorAndThrow(error); }
507
+ };
508
+
509
+ /**
510
+ * Gets a metafield matching a product variant namespace and key.
511
+ *
512
+ * @param {number} productId
513
+ * @param {string} namespace
514
+ * @param {string} key (Max key allowed is 30 characters)
515
+ * @returns Promise
516
+ */
517
+ public getMetafield = async (productId: number, variantId: number, namespace: string, key: string): Promise<IMetafield> => {
518
+ try {
519
+ Logger.info(`ShopifyVariantService - getMetafield -> Getting metafield for product id -> ${productId} and variant ${variantId} and namespace ${namespace} and key ${key}`);
520
+ const metafields = await this.getMetafields(variantId);
521
+ let foundMetafield: IMetafield = null;
522
+ if (metafields && metafields.length > 0) {
523
+ foundMetafield = metafields.find(m => m.namespace === namespace && m.key === key);
524
+ }
525
+ return foundMetafield;
526
+ } catch (error) { this.logErrorAndThrow(error); }
527
+ };
528
+
529
+ /**
530
+ * Adds metafields to variants
531
+ *
532
+ * @param {IVariantMetafield[]} variantMetafields
533
+ * @returns Promise Array of booleans taht says how the chuncks have been processed
534
+ */
535
+ public addMetafieldsToVariants = async (variantMetafields: IVariantMetafield[]): Promise<boolean[]> => {
536
+ try {
537
+ const chuncksResults: boolean[] = [];
538
+ Logger.info(`ShopifyVariantService - addMetafieldsToVariants -> Add metafields to variants: ${JSON.stringify(variantMetafields)}`);
539
+ const chuncks: IVariantMetafield[][] = this.sliceIntoChunks(variantMetafields, 50);
540
+ Logger.info(`ShopifyVariantService - addMetafieldsToVariants -> Add metafields to variants chuncks of 50: ${chuncks.length}`);
541
+ if (chuncks && chuncks.length > 0) {
542
+ for (const variantMetafieldsOfChunck of chuncks) {
543
+ const variantsInput: { id: string; metafields: any[] }[] = [];
544
+ for (const vM of variantMetafieldsOfChunck) {
545
+ const variantInput = { id: `gid://shopify/ProductVariant/${vM.variant_id}`, metafields: [] };
546
+ for (const m of vM.metafields) {
547
+ const metafield = {
548
+ namespace: m.namespace, key: m.key.length > 30 ? m.key.substring(0, 30) : m.key, value: `${m.value}`,
549
+ type: this.getMetafieldType(m.type, m.value), description: m.description
550
+ };
551
+ if (m.id) {
552
+ (metafield as any).id = `gid://shopify/Metafield/${m.id}`;
553
+ }
554
+ variantInput.metafields.push(metafield);
555
+ }
556
+ variantsInput.push(variantInput);
557
+ }
558
+ const rawQuery = `mutation variantsUpdate( ${variantMetafieldsOfChunck.map((variantMetafield) => (`$variant${variantMetafield.variant_id}: ProductVariantInput!`)).join(',')}){
559
+ ${variantMetafieldsOfChunck.map((variantMetafield) => (`
560
+ vMetafield${variantMetafield.variant_id}: productVariantUpdate(input: $variant${variantMetafield.variant_id}) {
561
+ productVariant { id }
562
+ userErrors { field message }
563
+ }`)).join('')} }`;
564
+ const query = gql`${rawQuery}`;
565
+ Logger.info('addMetafieldsToVariants query:');
566
+ Logger.info(rawQuery);
567
+ const variables: any = {};
568
+ for (const vM of variantMetafieldsOfChunck) {
569
+ variables[`variant${vM.variant_id}`] = variantsInput.find((vI) => vI.id === `gid://shopify/ProductVariant/${vM.variant_id}`);
570
+ }
571
+ Logger.info('ShopifyVariantService - addMetafieldsToVariants -> addMetafieldsToVariants variables:');
572
+ Logger.info(variables);
573
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(query), variables: variables }, { query_cost: 10 * variantMetafieldsOfChunck.length });
574
+ if (response && response.data && response.data.data) {
575
+ chuncksResults.push(true);
576
+ } else {
577
+ chuncksResults.push(false);
578
+ }
579
+ }
580
+ }
581
+ return chuncksResults;
582
+ } catch (error) { this.logErrorAndThrow(error); }
583
+ };
584
+ }
@@ -0,0 +1,37 @@
1
+ import * as express from 'express';
2
+ import { Logger } from '../../Logger';
3
+ import { HmacUtils } from '../..';
4
+ import { Request, Response } from 'express-serve-static-core';
5
+
6
+ const router = express.Router();
7
+
8
+ export class ShopifyMandatoryRouter {
9
+
10
+ constructor(private shopifySecret: string, private customerRedactcallback: ICustomerRedactCallback, private shopRedactcallback: IShopRedactCallback, private customersDataCallback: ICustomersDataCallback) { }
11
+
12
+ public buildRoutes(): express.Router {
13
+ router.post('/customers/redact', HmacUtils.jsonWebhookParser, (req, res) => { this.asyncFrame(req, res, this.customerRedactcallback); });
14
+ router.post('/customers/data_request', HmacUtils.jsonWebhookParser, (req, res) => { this.asyncFrame(req, res, this.customersDataCallback); });
15
+ router.post('/shop/redact', HmacUtils.jsonWebhookParser, (req, res) => { this.asyncFrame(req, res, this.shopRedactcallback); });
16
+ return router;
17
+ }
18
+
19
+ private asyncFrame = async (req: Request, res: Response, callback: (body: any) => Promise<express.Response | IOrder[]>): Promise<void> => {
20
+ return new Promise<void>(async (resolve, reject) => {
21
+ Logger.info('Mandatory webhook', req.body);
22
+ try {
23
+ const isHmacOk = await HmacUtils.checkHmac(req, this.shopifySecret);
24
+ if (!isHmacOk) throw 'Hmac not correct';
25
+ const response = await callback(req.body);
26
+ res.status(200).send(response);
27
+ resolve();
28
+ } catch (error) {
29
+ Logger.error(error);
30
+ res.status(401).send('HMAC validation failed');
31
+ reject(Error(error));
32
+ }
33
+ });
34
+ };
35
+ }
36
+
37
+ export default ShopifyMandatoryRouter;