@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,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
|
+
}
|