@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.
- package/Logger.ts +18 -0
- package/README.md +258 -0
- package/addTypings.sh +2 -0
- package/bitbucket-pipelines.yml +38 -0
- package/index.ts +132 -0
- package/package.json +57 -0
- package/publish.sh +20 -0
- package/services/CacheWrapper.ts +30 -0
- package/services/CountryCodeService.ts +507 -0
- package/shopify/ShopifyAppService.ts +109 -0
- package/shopify/ShopifyAssetService.ts +20 -0
- package/shopify/ShopifyBillingService.ts +73 -0
- package/shopify/ShopifyCartTrasnformationService.ts +207 -0
- package/shopify/ShopifyCollectionService.ts +523 -0
- package/shopify/ShopifyCustomerService.ts +472 -0
- package/shopify/ShopifyDeliveryCustomisationService.ts +220 -0
- package/shopify/ShopifyDiscountService.ts +131 -0
- package/shopify/ShopifyDraftOrderService.ts +125 -0
- package/shopify/ShopifyFulfillmentService.ts +41 -0
- package/shopify/ShopifyFunctionsProductDiscountsService.ts +166 -0
- package/shopify/ShopifyInventoryService.ts +415 -0
- package/shopify/ShopifyLocationService.ts +29 -0
- package/shopify/ShopifyOrderRefundsService.ts +138 -0
- package/shopify/ShopifyOrderRiskService.ts +19 -0
- package/shopify/ShopifyOrderService.ts +1143 -0
- package/shopify/ShopifyPageService.ts +62 -0
- package/shopify/ShopifyProductService.ts +772 -0
- package/shopify/ShopifyShippingZonesService.ts +37 -0
- package/shopify/ShopifyShopService.ts +101 -0
- package/shopify/ShopifyTemplateService.ts +30 -0
- package/shopify/ShopifyThemeService.ts +33 -0
- package/shopify/ShopifyUtils.ts +56 -0
- package/shopify/ShopifyWebhookService.ts +110 -0
- package/shopify/base/APIVersion.ts +4 -0
- package/shopify/base/AbstractService.ts +152 -0
- package/shopify/base/ErrorHelper.ts +24 -0
- package/shopify/errors/InspiraShopifyCustomError.ts +7 -0
- package/shopify/errors/InspiraShopifyError.ts +15 -0
- package/shopify/errors/InspiraShopifyUnableToReserveInventoryError.ts +7 -0
- package/shopify/helpers/ShopifyProductServiceHelper.ts +450 -0
- package/shopify/product/ShopifyProductCountService.ts +110 -0
- package/shopify/product/ShopifyProductListService.ts +333 -0
- package/shopify/product/ShopifyProductMetafieldsService.ts +405 -0
- package/shopify/product/ShopifyProductPublicationsService.ts +112 -0
- package/shopify/product/ShopifyVariantService.ts +584 -0
- package/shopify/router/ShopifyMandatoryRouter.ts +37 -0
- package/shopify/router/ShopifyRouter.ts +85 -0
- package/shopify/router/ShopifyRouterBis.ts +85 -0
- package/shopify/router/ShopifyRouterBisBis.ts +85 -0
- package/shopify/router/ShopifyRouterBisBisBis.ts +85 -0
- package/shopify/router/ShopifyRouterBisBisBisBis.ts +85 -0
- package/shopify/router/WebhookSkipMiddleware.ts +73 -0
- package/shopify/router/services/CryptoService.ts +26 -0
- package/shopify/router/services/HmacValidator.ts +36 -0
- package/shopify/router/services/OauthService.ts +17 -0
- package/shopify/router/services/RestUtils.ts +13 -0
- package/shopify/router/services/rateLimiter/MemoryStores.ts +46 -0
- package/shopify/router/services/rateLimiter/StoreRateLimiter.ts +46 -0
- package/test/README.md +223 -0
- package/test/router/ShopifyRouter.test.ts +71 -0
- package/test/router/WebhookSkipMiddleware.test.ts +86 -0
- package/test/router/services/HmacValidator.test.ts +24 -0
- package/test/router/services/RestUtils.test.ts +13 -0
- package/test/router/services/rateLimiter/StoreRateLimiter.test.ts +62 -0
- package/test/services/CacheWrapper.test.ts +30 -0
- package/test/shopify/ShopifyOrderService.test.ts +29 -0
- package/test/shopify/ShopifyProductService.test.ts +118 -0
- package/test/shopify/ShopifyWebhookService.test.ts +105 -0
- package/tsconfig.json +10 -0
- package/typings/axios.d.ts +8 -0
- package/typings/index.d.ts +1682 -0
|
@@ -0,0 +1,772 @@
|
|
|
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 { ShopifyProductServiceHelper } from './helpers/ShopifyProductServiceHelper';
|
|
7
|
+
import { AbstractService } from './base/AbstractService';
|
|
8
|
+
import { ShopifyProductPublicationsService } from './product/ShopifyProductPublicationsService';
|
|
9
|
+
import { ShopifyProductCountService } from './product/ShopifyProductCountService';
|
|
10
|
+
import { ShopifyProductListService } from './product/ShopifyProductListService';
|
|
11
|
+
import ErrorHelper from './base/ErrorHelper';
|
|
12
|
+
import { ShopifyProductMetafieldsService } from './product/ShopifyProductMetafieldsService';
|
|
13
|
+
import { ShopifyVariantService } from './product/ShopifyVariantService';
|
|
14
|
+
|
|
15
|
+
export class ShopifyProductService extends AbstractService {
|
|
16
|
+
|
|
17
|
+
public publications: ShopifyProductPublicationsService;
|
|
18
|
+
public count: ShopifyProductCountService;
|
|
19
|
+
public list: ShopifyProductListService;
|
|
20
|
+
public metafields: ShopifyProductMetafieldsService;
|
|
21
|
+
public variants: ShopifyVariantService;
|
|
22
|
+
private shopifyProductServiceHelper: ShopifyProductServiceHelper;
|
|
23
|
+
|
|
24
|
+
constructor(private axiosInstance: AxiosInstance) {
|
|
25
|
+
super();
|
|
26
|
+
this.publications = new ShopifyProductPublicationsService(axiosInstance);
|
|
27
|
+
this.count = new ShopifyProductCountService(axiosInstance);
|
|
28
|
+
this.list = new ShopifyProductListService(axiosInstance);
|
|
29
|
+
this.metafields = new ShopifyProductMetafieldsService(axiosInstance);
|
|
30
|
+
this.shopifyProductServiceHelper = new ShopifyProductServiceHelper();
|
|
31
|
+
this.variants = new ShopifyVariantService(axiosInstance, this.shopifyProductServiceHelper);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Product must specify the options names of the variants.<br>
|
|
36
|
+
* To assign images to variants the variants must have the image_id & product images list must have the variant_ids list.<br>
|
|
37
|
+
* Does not create more than 150 variants at a time.<br>
|
|
38
|
+
* Does not create more than 150 images.<br>
|
|
39
|
+
* When no locationID provided no stock is added.
|
|
40
|
+
*
|
|
41
|
+
*
|
|
42
|
+
* @param {IProduct} product
|
|
43
|
+
* @param {number} locationId
|
|
44
|
+
* @param {boolean} retrieveInventory
|
|
45
|
+
* @returns created product
|
|
46
|
+
*/
|
|
47
|
+
public create = async (product: IProduct, locationId: number, retrieveInventory: boolean): Promise<IProduct> => {
|
|
48
|
+
try {
|
|
49
|
+
Logger.info('create', JSON.stringify(product));
|
|
50
|
+
const productCreateQuery = ShopifyProductServiceHelper.graphQLQuery.getProductCreateMutation(retrieveInventory);
|
|
51
|
+
const productToCreate: IProductCreateRequest = {};
|
|
52
|
+
|
|
53
|
+
const mediaToCreate: { alt: string; mediaContentType: 'EXTERNAL_VIDEO' | 'IMAGE' | 'VIDEO', originalSource: string; }[] = [];
|
|
54
|
+
const mediaToCreateForVarinats: { alt: string; mediaContentType: 'EXTERNAL_VIDEO' | 'IMAGE' | 'VIDEO', originalSource: string; }[] = [];
|
|
55
|
+
if (product.variants && product.variants.length > 0) {
|
|
56
|
+
productToCreate.productOptions = [];
|
|
57
|
+
if (product.variants[0].option1) {
|
|
58
|
+
const optionValues: { name: string; }[] = [];
|
|
59
|
+
for (const v of product.variants) {
|
|
60
|
+
if (!optionValues.map(op => op.name).includes(v.option1)) {
|
|
61
|
+
optionValues.push({ name: v.option1 });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
productToCreate.productOptions.push({ position: 1, name: product.options[0].name, values: optionValues });
|
|
65
|
+
}
|
|
66
|
+
if (product.variants[0].option2) {
|
|
67
|
+
const optionValues: { name: string; }[] = [];
|
|
68
|
+
for (const v of product.variants) {
|
|
69
|
+
if (!optionValues.map(op => op.name).includes(v.option2)) {
|
|
70
|
+
optionValues.push({ name: v.option2 });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
productToCreate.productOptions.push({ position: 2, name: product.options[1].name, values: optionValues });
|
|
74
|
+
}
|
|
75
|
+
if (product.variants[0].option3) {
|
|
76
|
+
const optionValues: { name: string; }[] = [];
|
|
77
|
+
for (const v of product.variants) {
|
|
78
|
+
if (!optionValues.map(op => op.name).includes(v.option3)) {
|
|
79
|
+
optionValues.push({ name: v.option3 });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
productToCreate.productOptions.push({ position: 3, name: product.options[2].name, values: optionValues });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (product.vendor) {
|
|
86
|
+
productToCreate.vendor = product.vendor;
|
|
87
|
+
}
|
|
88
|
+
if (product.product_type) {
|
|
89
|
+
productToCreate.productType = product.product_type;
|
|
90
|
+
}
|
|
91
|
+
if (product.title) {
|
|
92
|
+
productToCreate.title = product.title;
|
|
93
|
+
}
|
|
94
|
+
if (product.tags) {
|
|
95
|
+
productToCreate.tags = Array.isArray(product.tags) ? product.tags : (product.tags ? product.tags.split(',').map(tag => tag.trim()) : []);
|
|
96
|
+
}
|
|
97
|
+
if (product.body_html) {
|
|
98
|
+
productToCreate.descriptionHtml = product.body_html;
|
|
99
|
+
}
|
|
100
|
+
if (product.images && product.images.length > 0) {
|
|
101
|
+
for (const img of product.images) {
|
|
102
|
+
if (img.src) {
|
|
103
|
+
if (!img.variant_ids || img.variant_ids.length === 0) {
|
|
104
|
+
mediaToCreate.push({ mediaContentType: 'IMAGE', alt: '', originalSource: img.src.trim().replace(/ /g, '%20') });
|
|
105
|
+
} else {
|
|
106
|
+
mediaToCreateForVarinats.push({ mediaContentType: 'IMAGE', alt: '', originalSource: img.src.trim().replace(/ /g, '%20') });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
productToCreate.status = 'ACTIVE';
|
|
112
|
+
if(product.status.toUpperCase() === 'ARCHIVED') {
|
|
113
|
+
productToCreate.status = 'ARCHIVED';
|
|
114
|
+
} else if(product.status.toUpperCase() === 'DRAFT') {
|
|
115
|
+
productToCreate.status = 'DRAFT';
|
|
116
|
+
}
|
|
117
|
+
Logger.info(`Product creating .... ${JSON.stringify(productToCreate)}`);
|
|
118
|
+
Logger.info(`Product media creating for no variant assigned .... ${JSON.stringify(mediaToCreate)}`);
|
|
119
|
+
Logger.info(`Product media creating for variants .... ${JSON.stringify(mediaToCreateForVarinats)}`);
|
|
120
|
+
const response = await this.axiosInstance.post('/graphql.json', { query: print(gql`${productCreateQuery}`), variables: { product: productToCreate, media: mediaToCreate } }, { query_cost: 50 });
|
|
121
|
+
|
|
122
|
+
if (response && response.data && response.data.data && response.data.data.productCreate && response.data.data.productCreate.product) {
|
|
123
|
+
const productGraph: ShopifygraphQl.IGenericProductResponse = response.data.data.productCreate.product;
|
|
124
|
+
|
|
125
|
+
const createdProduct: IProduct = this.shopifyProductServiceHelper.getProduct(productGraph);
|
|
126
|
+
if (createdProduct && createdProduct.id) {
|
|
127
|
+
Logger.info(`Product created with id ${createdProduct.id}`);
|
|
128
|
+
const variantsToCreate: ShopifygraphQl.IVariantToPost[] = [];
|
|
129
|
+
for (const variant of product.variants) {
|
|
130
|
+
let imageSrc = '';
|
|
131
|
+
if (variant.image_id) {
|
|
132
|
+
const originalImage = product.images.find(i => i.id === variant.image_id);
|
|
133
|
+
if (originalImage && originalImage.src) {
|
|
134
|
+
imageSrc = originalImage.src;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const variantToCreate: ShopifygraphQl.IVariantToPost = this.shopifyProductServiceHelper.getVariantToPost(variant, { imageSrc: imageSrc }, product.options.map(op => op.name), locationId);
|
|
138
|
+
variantsToCreate.push(variantToCreate);
|
|
139
|
+
}
|
|
140
|
+
const variantsCreateQuery = ShopifyProductServiceHelper.graphQLQuery.getVariantsBulkCreateMutation(true, locationId ? true : false, true);
|
|
141
|
+
Logger.info(`Product Variants create .... ${JSON.stringify(variantsToCreate)}`);
|
|
142
|
+
Logger.info(`Product Variants create query .... ${JSON.stringify(variantsCreateQuery)}`);
|
|
143
|
+
const variantsResponse = await this.axiosInstance.post('/graphql.json', { query: print(gql`${variantsCreateQuery}`), variables: { productId: this.getGraphProductIdFromId(createdProduct.id), variants: variantsToCreate, media: mediaToCreateForVarinats } }, { query_cost: 50 });
|
|
144
|
+
if (variantsResponse && variantsResponse.data && variantsResponse.data.data && variantsResponse.data.data.productVariantsBulkCreate && variantsResponse.data.data.productVariantsBulkCreate.productVariants) {
|
|
145
|
+
createdProduct.variants = [];
|
|
146
|
+
for (const graphVariant of variantsResponse.data.data.productVariantsBulkCreate.productVariants) {
|
|
147
|
+
createdProduct.variants.push(this.shopifyProductServiceHelper.getVariant(graphVariant));
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
Logger.error(`Product variants were not created. ${createdProduct.id}`);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
Logger.error(`Product was not created. ${createdProduct.id}`);
|
|
154
|
+
}
|
|
155
|
+
return createdProduct;
|
|
156
|
+
} else {
|
|
157
|
+
throw new InspiraShopifyError({ message: JSON.stringify(response.data.errors) });
|
|
158
|
+
}
|
|
159
|
+
} catch (error) { this.logErrorAndThrow(error); }
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {number} productId where to create the option
|
|
164
|
+
* @param {string} optionName
|
|
165
|
+
* @param {string[]} optionValues
|
|
166
|
+
* @param {number} position
|
|
167
|
+
*/
|
|
168
|
+
public createOption = async (productId: number, optionName: string, optionValues: string[], position: number): Promise<boolean> => {
|
|
169
|
+
try {
|
|
170
|
+
Logger.info(`ShopifyVariantService - createOption -> option Name ${optionName} --- position ${position} --- optionValues ${JSON.stringify(optionValues)}`);
|
|
171
|
+
|
|
172
|
+
const optionCreateQuery = ShopifyProductServiceHelper.graphQLQuery.productOptionCreateMutation();
|
|
173
|
+
Logger.info(`ShopifyVariantService - createOption -> query .... ${JSON.stringify(optionCreateQuery)}`);
|
|
174
|
+
const variables = { productId: this.getGraphProductIdFromId(productId), options: [{ name: optionName, position: position, values: optionValues.map(v => ({ name: v })) }] };
|
|
175
|
+
Logger.info(`ShopifyVariantService - createOption -> variables .... ${JSON.stringify(variables)}`);
|
|
176
|
+
|
|
177
|
+
const optionsResponse = await this.axiosInstance.post('/graphql.json', { query: print(gql`${optionCreateQuery}`), variables: variables }, { query_cost: 50 });
|
|
178
|
+
let isOptionCreated = false;
|
|
179
|
+
if (optionsResponse && optionsResponse.data && optionsResponse.data.data && optionsResponse.data.data.productOptionsCreate) {
|
|
180
|
+
isOptionCreated = true;
|
|
181
|
+
} else {
|
|
182
|
+
Logger.error(`ShopifyVariantService - createOption -> ${optionsResponse.data.data}`);
|
|
183
|
+
}
|
|
184
|
+
return isOptionCreated;
|
|
185
|
+
} catch (error) { this.logErrorAndThrow(error); }
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
public update = async (product: IProductUpdateRequest): Promise<void> => {
|
|
189
|
+
try {
|
|
190
|
+
Logger.info(`update product with id ${product.id}`, product);
|
|
191
|
+
const productUpdateQuery = ShopifyProductServiceHelper.graphQLQuery.updateProductMutation;
|
|
192
|
+
if (`${product.id}`.indexOf('gid') === -1) {
|
|
193
|
+
product.id = this.getGraphProductIdFromId(product.id as number);
|
|
194
|
+
}
|
|
195
|
+
Logger.info(`Product update query .... ${JSON.stringify(productUpdateQuery)}`);
|
|
196
|
+
Logger.info(`Product update variable .... ${JSON.stringify(product)}`);
|
|
197
|
+
const productUpdateResponse = await this.axiosInstance.post('/graphql.json', { query: print(gql`${productUpdateQuery}`), variables: { product: product } }, { query_cost: 10 });
|
|
198
|
+
if (!productUpdateResponse?.data?.data?.productUpdate?.product?.id) {
|
|
199
|
+
Logger.error(`Product did not update ${JSON.stringify(productUpdateResponse?.data?.data)}`);
|
|
200
|
+
throw new InspiraShopifyError({ message: `Product did not update ${JSON.stringify(productUpdateResponse?.data?.data?.productUpdate?.userErrors)}` });
|
|
201
|
+
}
|
|
202
|
+
} catch (error) { this.logErrorAndThrow(error); }
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
*
|
|
207
|
+
* @param cursor in the first request send null
|
|
208
|
+
* @param metafields
|
|
209
|
+
* @returns array of elements or empty array
|
|
210
|
+
*/
|
|
211
|
+
public getProductsWithMetafiledsInBatch = async (cursor: string, metafields: { namespace: string; key: string; }[]): Promise<IProductMetafieldInBatch[]> => {
|
|
212
|
+
try {
|
|
213
|
+
if (metafields && metafields.length > 0) {
|
|
214
|
+
Logger.info(`Get product with metafields from cursor: ${cursor}`);
|
|
215
|
+
const numOfProductsToGet = parseInt(`${200 / metafields.length}`, 10);
|
|
216
|
+
const query = gql`{
|
|
217
|
+
products(first: ${numOfProductsToGet}${cursor ? `,after: "${cursor}"` : ''}) {
|
|
218
|
+
edges {
|
|
219
|
+
cursor
|
|
220
|
+
node {
|
|
221
|
+
id
|
|
222
|
+
${metafields.map((m, i) => (`m${i}: metafield(namespace: "${m.namespace}", key: "${m.key}") {
|
|
223
|
+
value
|
|
224
|
+
}`)).join('')}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}`;
|
|
229
|
+
const response = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: numOfProductsToGet * 2 * metafields.length });
|
|
230
|
+
if (response && response.data && response.data.data && response.data.data.products) {
|
|
231
|
+
const productsGraph = response.data.data.products.edges;
|
|
232
|
+
const productsWithMetafields = productsGraph.map((p: any) => ({ id: this.getIdFromGraphId(p.node.id), cursor: p.cursor, metafields: metafields.map((m, i) => ({ namespace: m.namespace, key: m.key, value: p.node[`m${i}`] ? p.node[`m${i}`].value : null })) }));
|
|
233
|
+
return productsWithMetafields;
|
|
234
|
+
} else {
|
|
235
|
+
throw new InspiraShopifyError({ message: JSON.stringify(response.data.errors) });
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
Logger.info('No metafields provided');
|
|
239
|
+
}
|
|
240
|
+
} catch (error) { this.logErrorAndThrow(error); }
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Gets a product By Id with only stock information.
|
|
245
|
+
*
|
|
246
|
+
* @param {number} productId
|
|
247
|
+
* @returns Promise
|
|
248
|
+
*/
|
|
249
|
+
public getProductWithStockInfoById = async (productId: number): Promise<IProductStocks> => {
|
|
250
|
+
try {
|
|
251
|
+
Logger.info(`Get product stock with ID: ${productId}`);
|
|
252
|
+
const productStock: IProductStocks = await this.getProductWithStockInfoByIdFromCursor(productId, 50, null);
|
|
253
|
+
let lastVariantsSize: number = productStock.variants.length;
|
|
254
|
+
while (lastVariantsSize >= 50) {
|
|
255
|
+
const productStockAlt: IProductStocks = await this.getProductWithStockInfoByIdFromCursor(productId, 50, productStock.variants[productStock.variants.length - 1].cursor);
|
|
256
|
+
lastVariantsSize = productStockAlt.variants.length;
|
|
257
|
+
productStock.variants = productStock.variants.concat(productStockAlt.variants);
|
|
258
|
+
}
|
|
259
|
+
return productStock;
|
|
260
|
+
} catch (error) { this.logErrorAndThrow(error); }
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Gets a product By Id with only stock information with 50 variants. Starts on a variant specified cursor from another search.
|
|
265
|
+
*
|
|
266
|
+
* @param {number} productId
|
|
267
|
+
* @returns Promise
|
|
268
|
+
*/
|
|
269
|
+
public getProductWithStockInfoByIdFromCursor = async (productId: number, numOfVariants: number, cursor: string): Promise<IProductStocks> => {
|
|
270
|
+
try {
|
|
271
|
+
Logger.info(`getProductWithStockInfoByIdFromCursor -> Get product stock with ID: ${productId} num of variants ${numOfVariants} and cursor ${cursor}`);
|
|
272
|
+
const query = gql`{
|
|
273
|
+
product(id: "${this.getGraphProductIdFromId(productId)}") {
|
|
274
|
+
id
|
|
275
|
+
variants(first: ${numOfVariants}, ${cursor ? `after: "${cursor}"` : ''}) {
|
|
276
|
+
edges {
|
|
277
|
+
cursor
|
|
278
|
+
node {
|
|
279
|
+
id
|
|
280
|
+
inventoryQuantity
|
|
281
|
+
inventoryPolicy
|
|
282
|
+
inventoryItem {
|
|
283
|
+
id
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}`;
|
|
290
|
+
const response = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: 110 });
|
|
291
|
+
if (response && response.data && response.data.data && response.data.data.product) {
|
|
292
|
+
const productGraph = response.data.data.product;
|
|
293
|
+
return {
|
|
294
|
+
id: productId, variants: productGraph.variants.edges.map((gv) => ({
|
|
295
|
+
id: this.getIdFromGraphId(gv.node.id),
|
|
296
|
+
cursor: gv.cursor,
|
|
297
|
+
inventory_quantity: gv.node.inventoryQuantity,
|
|
298
|
+
inventory_policy: gv.node.inventoryPolicy,
|
|
299
|
+
inventory_item_id: this.getIdFromGraphId(gv.node.inventoryItem.id)
|
|
300
|
+
} as IVariantStock))
|
|
301
|
+
};
|
|
302
|
+
} else {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
} catch (error) { this.logErrorAndThrow(error); }
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Add tags to a list of products.
|
|
310
|
+
*
|
|
311
|
+
* @param {number[]} productIds
|
|
312
|
+
* @returns Promise
|
|
313
|
+
*/
|
|
314
|
+
public addTagsToProducts = async (productIds: number[], tagsToAdd: string): Promise<any[]> => {
|
|
315
|
+
try {
|
|
316
|
+
if (productIds && productIds.length > 0) {
|
|
317
|
+
const errors = [];
|
|
318
|
+
Logger.info(`Add tags to products: ${productIds.toString()}`);
|
|
319
|
+
const productIdschunks: number[][] = this.sliceIntoChunks(productIds, 30);
|
|
320
|
+
for (const productIdsChunk of productIdschunks) {
|
|
321
|
+
Logger.info(`Add tags to products chunck with: ${productIdsChunk.length} products `);
|
|
322
|
+
const query = gql`mutation {
|
|
323
|
+
${productIdsChunk.map((id) => (`
|
|
324
|
+
Product${id}: tagsAdd(id: "${this.getGraphProductIdFromId(id)}", tags: "${this.escapeQuotes(tagsToAdd)}") {
|
|
325
|
+
node {
|
|
326
|
+
id
|
|
327
|
+
}
|
|
328
|
+
userErrors {
|
|
329
|
+
field
|
|
330
|
+
message
|
|
331
|
+
}
|
|
332
|
+
}`)).join('')}
|
|
333
|
+
}`;
|
|
334
|
+
const response = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: productIdsChunk.length * 15 });
|
|
335
|
+
|
|
336
|
+
Logger.info(`Add tags to products response: ${JSON.stringify(response.data)} `);
|
|
337
|
+
if (response && response.data && response.data.data) {
|
|
338
|
+
const updatesResponse = response.data.data;
|
|
339
|
+
for (const key in updatesResponse) {
|
|
340
|
+
const err = updatesResponse[key].userErrors;
|
|
341
|
+
if (err && err.length > 0) {
|
|
342
|
+
errors.push(err);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
errors.push('No response arrived');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return errors;
|
|
350
|
+
} else {
|
|
351
|
+
Logger.info('Add tags to products without product IDs provided');
|
|
352
|
+
return [];
|
|
353
|
+
}
|
|
354
|
+
} catch (error) { this.logErrorAndThrow(error); }
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Remove tags to a list of products
|
|
359
|
+
*
|
|
360
|
+
* @param {number[]} productIds
|
|
361
|
+
* @returns Promise
|
|
362
|
+
*/
|
|
363
|
+
public removeTagsToProducts = async (productIds: number[], tagsToRemove: string): Promise<any[]> => {
|
|
364
|
+
try {
|
|
365
|
+
if (productIds && productIds.length > 0) {
|
|
366
|
+
const errors = [];
|
|
367
|
+
Logger.info(`Remove tags to products: ${productIds.toString()}`);
|
|
368
|
+
const productIdschunks: number[][] = this.sliceIntoChunks(productIds, 30);
|
|
369
|
+
for (const productIdsChunk of productIdschunks) {
|
|
370
|
+
const query = gql`mutation { ${productIdsChunk.map((id) => (`
|
|
371
|
+
Product${id}: tagsRemove(id: "${this.getGraphProductIdFromId(id)}", tags: "${this.escapeQuotes(tagsToRemove)}") {
|
|
372
|
+
node {
|
|
373
|
+
id
|
|
374
|
+
}
|
|
375
|
+
userErrors {
|
|
376
|
+
field
|
|
377
|
+
message
|
|
378
|
+
}
|
|
379
|
+
}`)).join('')}
|
|
380
|
+
}`;
|
|
381
|
+
const response = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: productIdsChunk.length * 15 });
|
|
382
|
+
if (response && response.data && response.data.data) {
|
|
383
|
+
const updatesResponse = response.data.data;
|
|
384
|
+
for (const key in updatesResponse) {
|
|
385
|
+
const err = updatesResponse[key].userErrors;
|
|
386
|
+
if (err && err.length > 0) {
|
|
387
|
+
errors.push(err);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
errors.push('No response arrived');
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return errors;
|
|
395
|
+
} else {
|
|
396
|
+
Logger.info('Remove tags to products without Product IDs');
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
} catch (error) { this.logErrorAndThrow(error); }
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Gets a product By Id with only stock information. It retrieves up to 20 variants and 20 invenotry items ids
|
|
404
|
+
*
|
|
405
|
+
* @param {number} productId
|
|
406
|
+
* @returns Promise
|
|
407
|
+
*/
|
|
408
|
+
public getProductWithInventoryInfoById = async (productId: number): Promise<IProductInventory> => {
|
|
409
|
+
try {
|
|
410
|
+
Logger.info(`Get product Inventory stock with ID: ${productId}`);
|
|
411
|
+
const query = gql` {
|
|
412
|
+
product(id: "${this.getGraphProductIdFromId(productId)}") {
|
|
413
|
+
id
|
|
414
|
+
variants(first: 20) {
|
|
415
|
+
edges {
|
|
416
|
+
node {
|
|
417
|
+
id
|
|
418
|
+
price
|
|
419
|
+
sku
|
|
420
|
+
availableForSale
|
|
421
|
+
inventoryQuantity
|
|
422
|
+
inventoryPolicy
|
|
423
|
+
inventoryItem {
|
|
424
|
+
id
|
|
425
|
+
tracked
|
|
426
|
+
unitCost {
|
|
427
|
+
amount
|
|
428
|
+
}
|
|
429
|
+
inventoryLevels(first: 20) {
|
|
430
|
+
edges {
|
|
431
|
+
node {
|
|
432
|
+
quantities(names:["available"]){
|
|
433
|
+
quantity
|
|
434
|
+
}
|
|
435
|
+
location {
|
|
436
|
+
id
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}`;
|
|
447
|
+
const response = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: 883 });
|
|
448
|
+
if (response && response.data && response.data.data && response.data.data.product) {
|
|
449
|
+
const productGraph = response.data.data.product;
|
|
450
|
+
return {
|
|
451
|
+
id: productId,
|
|
452
|
+
variants: productGraph.variants.edges.map((gv) => ({
|
|
453
|
+
id: this.getIdFromGraphId(gv.node.id),
|
|
454
|
+
inventory_quantity: gv.node.inventoryQuantity,
|
|
455
|
+
inventory_policy: gv.node.inventoryPolicy,
|
|
456
|
+
sku: gv.node.sku,
|
|
457
|
+
available_for_sale: gv.node.availableForSale,
|
|
458
|
+
inventory_item_id: this.getIdFromGraphId(gv.node.inventoryItem.id),
|
|
459
|
+
inventory_item: {
|
|
460
|
+
id: this.getIdFromGraphId(gv.node.inventoryItem.id),
|
|
461
|
+
tracked: gv.node.inventoryItem.tracked,
|
|
462
|
+
cost: gv.node.inventoryItem.unitCost ? gv.node.inventoryItem.unitCost.amount : null,
|
|
463
|
+
levels: gv.node.inventoryItem.inventoryLevels.edges.map((gl) => ({
|
|
464
|
+
location_id: this.getIdFromGraphId(gl.node.location.id),
|
|
465
|
+
id: this.getIdFromGraphId(gl.node.id),
|
|
466
|
+
available: gl.node.quantities && gl.node.quantities[0] ? gl.node.quantities[0].quantity : 0,
|
|
467
|
+
inventory_item_id: this.getIdFromGraphId(gv.node.inventoryItem.id)
|
|
468
|
+
} as IInventoryLevel)
|
|
469
|
+
)
|
|
470
|
+
} as IInventoryItem
|
|
471
|
+
} as IVariantInventory)
|
|
472
|
+
)
|
|
473
|
+
};
|
|
474
|
+
} else {
|
|
475
|
+
Logger.error(`getProductWithInventoryInfoById not the expected response. Data ${JSON.stringify(response.data.data)}. Error is ${JSON.stringify(response.data.errors)}`);
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
} catch (error) { this.logErrorAndThrow(error); }
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Gets a product By SKU
|
|
483
|
+
*
|
|
484
|
+
* @param {string} sku
|
|
485
|
+
* @returns Promise<IProduct[]>
|
|
486
|
+
*/
|
|
487
|
+
public getBySKU = async (sku: string): Promise<IProduct[]> => {
|
|
488
|
+
try {
|
|
489
|
+
Logger.info(`getBySKU - Get Product by SKU - ${sku}`);
|
|
490
|
+
const query = gql`query {
|
|
491
|
+
products(first: 10, query: "sku:${sku}") {
|
|
492
|
+
id
|
|
493
|
+
title
|
|
494
|
+
vendor
|
|
495
|
+
handle
|
|
496
|
+
productType
|
|
497
|
+
tags
|
|
498
|
+
publishedAt
|
|
499
|
+
updatedAt
|
|
500
|
+
hasOnlyDefaultVariant
|
|
501
|
+
isGiftCard
|
|
502
|
+
status
|
|
503
|
+
}
|
|
504
|
+
}`;
|
|
505
|
+
const response = await this.axiosInstance.post('/graphql.json', { query: print(query) });
|
|
506
|
+
Logger.info(`getBySKU - Get Product by SKU - Query Response - ${response}`);
|
|
507
|
+
if (response && response.data && response.data.data && response.data.data.products) {
|
|
508
|
+
const productsGraph = response.data.data.products.nodes;
|
|
509
|
+
const mappedResponse: IProduct[] = productsGraph.map(el => ({
|
|
510
|
+
id: el.id,
|
|
511
|
+
title: el.title,
|
|
512
|
+
vendor: el.vendor,
|
|
513
|
+
handle: el.handle,
|
|
514
|
+
productType: el.productType,
|
|
515
|
+
tags: el.tags,
|
|
516
|
+
publishedAt: el.publishedAt,
|
|
517
|
+
updatedAt: el.updatedAt,
|
|
518
|
+
hasOnlyDefaultVariant: el.hasOnlyDefaultVariant,
|
|
519
|
+
isGiftCard: el.isGiftCard,
|
|
520
|
+
status: el.status,
|
|
521
|
+
}));
|
|
522
|
+
return mappedResponse;
|
|
523
|
+
} else {
|
|
524
|
+
throw new InspiraShopifyError({ message: JSON.stringify(response.data.errors) });
|
|
525
|
+
}
|
|
526
|
+
} catch (error) {
|
|
527
|
+
this.logErrorAndThrow(error);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
public delete = async (id: number): Promise<void> => {
|
|
532
|
+
try {
|
|
533
|
+
Logger.info(`ShopifyProductService - delete -> ${id}`);
|
|
534
|
+
const rawQuery = `mutation {
|
|
535
|
+
productDelete(input: { id: "${this.getGraphProductIdFromId(id)}"}) {
|
|
536
|
+
deletedProductId
|
|
537
|
+
userErrors {
|
|
538
|
+
field
|
|
539
|
+
message
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}`;
|
|
543
|
+
Logger.info(`ShopifyProductService - delete -> rawQuery ${rawQuery}`);
|
|
544
|
+
const query = gql`${rawQuery}`;
|
|
545
|
+
const response = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: 10 });
|
|
546
|
+
if (response && response.data && response.data.data && response.data.data.productDelete) {
|
|
547
|
+
return;
|
|
548
|
+
} else {
|
|
549
|
+
Logger.error(`ShopifyProductService - delete -> Data ${JSON.stringify(response.data.data)}. Error is ${JSON.stringify(response.data.errors)}`);
|
|
550
|
+
throw new InspiraShopifyError({ message: `Product was not deleted - ${JSON.stringify(response.data.errors)}` });
|
|
551
|
+
}
|
|
552
|
+
} catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); throw new InspiraShopifyError(error); }
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
public getProductsByIdsWithCollectionsInfo = async (ids: number[], collectionIds: number[]): Promise<IProductCollection[]> => {
|
|
556
|
+
try {
|
|
557
|
+
Logger.info(`getProductsByIdsWithCollectionsInfo: productIds ${ids} collectionIds: ${collectionIds}`);
|
|
558
|
+
const rawQuery = ` {
|
|
559
|
+
products(first: 50, query: "${ids.map(id => `(id:${id})`).join(' OR ')}") {
|
|
560
|
+
edges {
|
|
561
|
+
node {
|
|
562
|
+
id
|
|
563
|
+
tags
|
|
564
|
+
vendor
|
|
565
|
+
productType
|
|
566
|
+
${collectionIds.map(cId => `
|
|
567
|
+
c_${cId}: inCollection(id: "gid://shopify/Collection/${cId}")`).join('')}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}`;
|
|
572
|
+
Logger.info(`getProductsByIdsWithCollectionsInfo - rawQuery ${rawQuery}`);
|
|
573
|
+
const query = gql`${rawQuery}`;
|
|
574
|
+
const response = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: 900 });
|
|
575
|
+
if (response && response.data && response.data.data && response.data.data.products) {
|
|
576
|
+
const productsGraph: IProductCollection[] = response.data.data.products.edges.map(p => {
|
|
577
|
+
const product: IProductCollection = { id: this.getIdFromGraphId(p.node.id), vendor: p.node.vendor, productType: p.node.productType, tags: p.node.tags, collectionsContained: collectionIds.map(cId => p.node[`c_${cId}`] ? cId : null).filter(c => c !== null) };
|
|
578
|
+
return product;
|
|
579
|
+
});
|
|
580
|
+
return productsGraph;
|
|
581
|
+
} else {
|
|
582
|
+
Logger.error(`getProductsByIdsWithCollectionsInfo not the expected response. Data ${JSON.stringify(response.data.data)}. Error is ${JSON.stringify(response.data.errors)}`);
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
} catch (error) { this.logErrorAndThrow(error); }
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Finds product by titleand returns the first 100 matches.
|
|
590
|
+
*
|
|
591
|
+
* @param {string} title
|
|
592
|
+
*
|
|
593
|
+
* @returns Promise
|
|
594
|
+
*/
|
|
595
|
+
public searchByTitle = async (title: string): Promise<{ title: string; id: number; handle: string; image: string; variants_count: number; variants: { id: number; title: string; }; }[]> => {
|
|
596
|
+
try {
|
|
597
|
+
Logger.info(`searchByTitle ${title}`);
|
|
598
|
+
const metafieldQuery = gql`query {
|
|
599
|
+
products(first: 100, query: "title:*${title}*") {
|
|
600
|
+
edges {
|
|
601
|
+
node {
|
|
602
|
+
title
|
|
603
|
+
id
|
|
604
|
+
handle
|
|
605
|
+
featuredImage {
|
|
606
|
+
url
|
|
607
|
+
}
|
|
608
|
+
variants(first: 100) {
|
|
609
|
+
edges {
|
|
610
|
+
node {
|
|
611
|
+
id
|
|
612
|
+
title
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}`;
|
|
620
|
+
|
|
621
|
+
const response = await this.axiosInstance.post('/graphql.json', { query: print(metafieldQuery), variables: {} }, { query_cost: 200 });
|
|
622
|
+
|
|
623
|
+
if (response && response.data && response.data.data && response.data.data.products && response.data.data.products.edges.length > 0) {
|
|
624
|
+
return response.data.data.products.edges.map((p) => ({ id: this.getIdFromGraphId(p.node.id), title: p.node.title, handle: p.node.handle, image: p.node.featuredImage ? p.node.featuredImage.url : '', variants_count: p.node.variants.edges.length, variants: p.node.variants.edges.map(v => ({ id: this.getIdFromGraphId(v.node.id), title: v.node.title })) }));
|
|
625
|
+
} else {
|
|
626
|
+
return [];
|
|
627
|
+
}
|
|
628
|
+
} catch (error) { this.logErrorAndThrow(error); }
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
*
|
|
633
|
+
* Create a product image and link it to variants.
|
|
634
|
+
*
|
|
635
|
+
* @param {number} productId - product ID
|
|
636
|
+
* @param { alt: string; src: string; } img of image
|
|
637
|
+
* @param {number[]} variantIds of image
|
|
638
|
+
*
|
|
639
|
+
* @returns Promise
|
|
640
|
+
*/
|
|
641
|
+
public createImage = async (productId: number, img: { alt?: string; src: string; }, variantIds: number[]): Promise<void> => {
|
|
642
|
+
try {
|
|
643
|
+
if (img.src) {
|
|
644
|
+
Logger.info(`Creating image for product id -> ${productId}, src ${img.src} for variants ${variantIds}`);
|
|
645
|
+
if (variantIds?.length > 0) {
|
|
646
|
+
const rawQuery = `mutation productVariantsBulkUpdate($productId: ID!, $variants: [ProductVariantsBulkInput!]!, $media: [CreateMediaInput!]) {
|
|
647
|
+
productVariantsBulkUpdate(productId: $productId, variants: $variants, media: $media)
|
|
648
|
+
{ product { id }
|
|
649
|
+
}
|
|
650
|
+
}`;
|
|
651
|
+
const addImageResponse = await this.axiosInstance.post('/graphql.json', { query: print(gql`${rawQuery}`), variables: { productId: this.getGraphProductIdFromId(productId), variants: variantIds.map(vid => ({ id: this.getGraphProductVariantIdFromId(vid), mediaSrc: [img.src] })), media: [{ originalSource: img.src.trim().replace(/ /g, '%20'), mediaContentType: 'IMAGE', alt: img.alt ? img.alt : '' }] } }, { query_cost: 60 });
|
|
652
|
+
if (addImageResponse && addImageResponse.data && addImageResponse.data.data?.productVariantsBulkUpdate.product?.id) {
|
|
653
|
+
Logger.info(`Added image ${img.src} to products ${productId} and linke to variants ${variantIds}`);
|
|
654
|
+
} else {
|
|
655
|
+
throw new InspiraShopifyError({ message: `No images have been added to product ${productId} - response ${JSON.stringify(addImageResponse.data)}` });
|
|
656
|
+
}
|
|
657
|
+
} else {
|
|
658
|
+
const rawQuery = `mutation productUpdate($input: ProductInput!, $media: [CreateMediaInput!]) {
|
|
659
|
+
productUpdate(input: $input, media: $media)
|
|
660
|
+
{ product {
|
|
661
|
+
media(first: 5, sortKey: ID ) {
|
|
662
|
+
nodes {
|
|
663
|
+
id
|
|
664
|
+
alt
|
|
665
|
+
mediaContentType
|
|
666
|
+
preview {
|
|
667
|
+
status
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}`;
|
|
674
|
+
const addImageResponse = await this.axiosInstance.post('/graphql.json', { query: print(gql`${rawQuery}`), variables: { input: { id: this.getGraphProductIdFromId(productId) }, media: [{ originalSource: img.src.trim().replace(/ /g, '%20'), mediaContentType: 'IMAGE', alt: img.alt ? img.alt : '' }] } }, { query_cost: 60 });
|
|
675
|
+
|
|
676
|
+
if (addImageResponse && addImageResponse.data && addImageResponse.data.data?.productUpdate && addImageResponse.data.data?.productUpdate?.product) {
|
|
677
|
+
Logger.info(`Added image ${img.src} to products ${productId}`);
|
|
678
|
+
} else {
|
|
679
|
+
throw new InspiraShopifyError({ message: `No images have been added to product ${productId} - response ${JSON.stringify(addImageResponse.data)}` });
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
} else {
|
|
684
|
+
throw new InspiraShopifyError({ message: `No image source provided to add to product ${productId}` });
|
|
685
|
+
}
|
|
686
|
+
} catch (error) { this.logErrorAndThrow(error); }
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
*
|
|
691
|
+
* Create a product image and link it to variants.
|
|
692
|
+
*
|
|
693
|
+
* @param {number} productId - product ID
|
|
694
|
+
*
|
|
695
|
+
* @returns Promise
|
|
696
|
+
*/
|
|
697
|
+
public deleteMedia = async (productId: number): Promise<void> => {
|
|
698
|
+
try {
|
|
699
|
+
Logger.info(`Delete product Media product id -> ${productId}`);
|
|
700
|
+
const media = await this.getMedia(productId);
|
|
701
|
+
if (media.length) {
|
|
702
|
+
const rawQuery = `mutation productDeleteMedia($mediaIds: [ID!]!, $productId: ID!) {
|
|
703
|
+
productDeleteMedia(mediaIds: $mediaIds, productId: $productId) {
|
|
704
|
+
deletedMediaIds
|
|
705
|
+
deletedProductImageIds
|
|
706
|
+
mediaUserErrors {
|
|
707
|
+
field
|
|
708
|
+
message
|
|
709
|
+
}
|
|
710
|
+
product {
|
|
711
|
+
id
|
|
712
|
+
title
|
|
713
|
+
media(first: 5) {
|
|
714
|
+
nodes {
|
|
715
|
+
alt
|
|
716
|
+
mediaContentType
|
|
717
|
+
status
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}`;
|
|
723
|
+
const deleteImageResponse = await this.axiosInstance.post('/graphql.json', { query: print(gql`${rawQuery}`), variables: { mediaIds: media.map(m => m.id), productId: this.getGraphProductIdFromId(productId) } }, { query_cost: 60 });
|
|
724
|
+
|
|
725
|
+
if (deleteImageResponse && deleteImageResponse.data && deleteImageResponse.data.data?.productDeleteMedia && deleteImageResponse.data.data?.productDeleteMedia) {
|
|
726
|
+
Logger.info(`Deleted media from product ${productId}. Images deleted -> ${deleteImageResponse.data.data?.productDeleteMedia.deletedProductImageIds}. Media deleted -> ${deleteImageResponse.data.data?.productDeleteMedia.deletedMediaIds}`);
|
|
727
|
+
} else {
|
|
728
|
+
throw new InspiraShopifyError({ message: `No images have been deleted on product ${productId} - response ${JSON.stringify(deleteImageResponse.data)}` });
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
} catch (error) { this.logErrorAndThrow(error); }
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
*
|
|
736
|
+
* Returns media of a product. Max of 250
|
|
737
|
+
*
|
|
738
|
+
* @param {number} productId - product ID
|
|
739
|
+
*
|
|
740
|
+
* @returns Promise
|
|
741
|
+
*/
|
|
742
|
+
public getMedia = async (productId: number): Promise<{ id: string; mediaContentType: 'IMAGE' | 'VIDEO'; }[]> => {
|
|
743
|
+
try {
|
|
744
|
+
Logger.info(`Get product Media product id -> ${productId}`);
|
|
745
|
+
const rawQuery = `query ProductMedia($productId: ID!) {
|
|
746
|
+
product(id: $productId) {
|
|
747
|
+
media(first: 250) {
|
|
748
|
+
edges {
|
|
749
|
+
node {
|
|
750
|
+
id
|
|
751
|
+
mediaContentType
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}`;
|
|
757
|
+
const getMediaResponse = await this.axiosInstance.post('/graphql.json', { query: print(gql`${rawQuery}`), variables: { productId: this.getGraphProductIdFromId(productId) } }, { query_cost: 60 });
|
|
758
|
+
|
|
759
|
+
if (getMediaResponse && getMediaResponse.data && getMediaResponse.data.data?.product && getMediaResponse.data.data?.product) {
|
|
760
|
+
Logger.info(`Get media from product ${productId}. ${JSON.stringify(getMediaResponse.data.data)}`);
|
|
761
|
+
if (getMediaResponse.data.data?.product?.media?.edges?.length > 0) {
|
|
762
|
+
return getMediaResponse.data.data?.product?.media?.edges.map((edge: { node: { id: string; mediaContentType: string; } }) => edge.node);
|
|
763
|
+
} else {
|
|
764
|
+
return [];
|
|
765
|
+
}
|
|
766
|
+
} else {
|
|
767
|
+
throw new InspiraShopifyError({ message: `No images could be retrieved for product ${productId} - response ${JSON.stringify(getMediaResponse.data)}` });
|
|
768
|
+
}
|
|
769
|
+
} catch (error) { this.logErrorAndThrow(error); }
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
}
|