@foxtware/mineral 1.0.5 → 1.0.7

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 (87) hide show
  1. package/.creds.yml.sample +1 -0
  2. package/_build_scripts/createNewFunction.js +23 -41
  3. package/api/UTILS.md +1 -1
  4. package/api/bleckmann/bleckmannPickticketGet.js +4 -2
  5. package/api/github/githubActionsSecretCreateOrUpdate.js +139 -0
  6. package/api/github/githubActionsSecretPublicKeyGet.js +59 -0
  7. package/api/google/googlecalendarEventCreate.js +2 -0
  8. package/api/linear/linearTeamGet.js +6 -3
  9. package/api/shopify/shopify.constants.js +25 -0
  10. package/api/shopify/shopify.utils.js +41 -6
  11. package/api/shopify/shopifyInventoryQuantitiesSet.js +164 -0
  12. package/api/shopify/shopifyMutationDo.js +12 -6
  13. package/api/shopify/shopifyShopGet.js +68 -0
  14. package/api/shopify/shopifyStorefrontArticleGet.js +86 -0
  15. package/api/shopify/shopifyStorefrontArticlesGet.js +67 -0
  16. package/api/shopify/shopifyStorefrontBlogGet.js +110 -0
  17. package/api/shopify/shopifyStorefrontBlogsGet.js +66 -0
  18. package/api/shopify/shopifyStorefrontCartAttributesUpdate.js +87 -0
  19. package/api/shopify/shopifyStorefrontCartBuyerIdentityUpdate.js +87 -0
  20. package/api/shopify/shopifyStorefrontCartCreate.js +83 -0
  21. package/api/shopify/shopifyStorefrontCartDeliveryAddressesAdd.js +87 -0
  22. package/api/shopify/shopifyStorefrontCartDeliveryAddressesRemove.js +87 -0
  23. package/api/shopify/shopifyStorefrontCartDeliveryAddressesReplace.js +87 -0
  24. package/api/shopify/shopifyStorefrontCartDeliveryAddressesUpdate.js +87 -0
  25. package/api/shopify/shopifyStorefrontCartDiscountCodesUpdate.js +87 -0
  26. package/api/shopify/shopifyStorefrontCartGet.js +100 -0
  27. package/api/shopify/shopifyStorefrontCartGiftCardCodesAdd.js +87 -0
  28. package/api/shopify/shopifyStorefrontCartGiftCardCodesRemove.js +87 -0
  29. package/api/shopify/shopifyStorefrontCartGiftCardCodesUpdate.js +87 -0
  30. package/api/shopify/shopifyStorefrontCartLinesAdd.js +87 -0
  31. package/api/shopify/shopifyStorefrontCartLinesRemove.js +87 -0
  32. package/api/shopify/shopifyStorefrontCartLinesUpdate.js +87 -0
  33. package/api/shopify/shopifyStorefrontCartMetafieldDelete.js +69 -0
  34. package/api/shopify/shopifyStorefrontCartMetafieldsSet.js +80 -0
  35. package/api/shopify/shopifyStorefrontCartNoteUpdate.js +87 -0
  36. package/api/shopify/shopifyStorefrontCartSelectedDeliveryOptionsUpdate.js +87 -0
  37. package/api/shopify/shopifyStorefrontCollectionGet.js +92 -0
  38. package/api/shopify/shopifyStorefrontCollectionsGet.js +66 -0
  39. package/api/shopify/shopifyStorefrontCustomerAccessTokenCreate.js +75 -0
  40. package/api/shopify/shopifyStorefrontCustomerAccessTokenCreateWithMultipass.js +72 -0
  41. package/api/shopify/shopifyStorefrontCustomerAccessTokenDelete.js +67 -0
  42. package/api/shopify/shopifyStorefrontCustomerAccessTokenRenew.js +72 -0
  43. package/api/shopify/shopifyStorefrontCustomerActivate.js +98 -0
  44. package/api/shopify/shopifyStorefrontCustomerActivateByUrl.js +91 -0
  45. package/api/shopify/shopifyStorefrontCustomerAddressCreate.js +94 -0
  46. package/api/shopify/shopifyStorefrontCustomerAddressDelete.js +78 -0
  47. package/api/shopify/shopifyStorefrontCustomerAddressUpdate.js +101 -0
  48. package/api/shopify/shopifyStorefrontCustomerCreate.js +79 -0
  49. package/api/shopify/shopifyStorefrontCustomerDefaultAddressUpdate.js +89 -0
  50. package/api/shopify/shopifyStorefrontCustomerGet.js +92 -0
  51. package/api/shopify/shopifyStorefrontCustomerRecover.js +64 -0
  52. package/api/shopify/shopifyStorefrontCustomerReset.js +98 -0
  53. package/api/shopify/shopifyStorefrontCustomerResetByUrl.js +91 -0
  54. package/api/shopify/shopifyStorefrontCustomerUpdate.js +93 -0
  55. package/api/shopify/shopifyStorefrontGet.js +233 -0
  56. package/api/shopify/shopifyStorefrontLocalizationGet.js +98 -0
  57. package/api/shopify/shopifyStorefrontLocationsGet.js +70 -0
  58. package/api/shopify/shopifyStorefrontMenuGet.js +93 -0
  59. package/api/shopify/shopifyStorefrontMetaobjectGet.js +126 -0
  60. package/api/shopify/shopifyStorefrontMetaobjectsGet.js +73 -0
  61. package/api/shopify/shopifyStorefrontMutationDo.js +103 -0
  62. package/api/shopify/shopifyStorefrontNodeGet.js +90 -0
  63. package/api/shopify/shopifyStorefrontNodesGet.js +86 -0
  64. package/api/shopify/shopifyStorefrontPageGet.js +93 -0
  65. package/api/shopify/shopifyStorefrontPagesGet.js +66 -0
  66. package/api/shopify/shopifyStorefrontPaymentSettingsGet.js +79 -0
  67. package/api/shopify/shopifyStorefrontPredictiveSearch.js +138 -0
  68. package/api/shopify/shopifyStorefrontProductGet.js +94 -0
  69. package/api/shopify/shopifyStorefrontProductRecommendationsGet.js +112 -0
  70. package/api/shopify/shopifyStorefrontProductTagsGet.js +80 -0
  71. package/api/shopify/shopifyStorefrontProductTypesGet.js +80 -0
  72. package/api/shopify/shopifyStorefrontProductsGet.js +76 -0
  73. package/api/shopify/shopifyStorefrontPublicApiVersionsGet.js +75 -0
  74. package/api/shopify/shopifyStorefrontSearch.js +8 -2
  75. package/api/shopify/shopifyStorefrontShopGet.js +81 -0
  76. package/api/shopify/shopifyStorefrontShopPayPaymentRequestSessionCreate.js +91 -0
  77. package/api/shopify/shopifyStorefrontShopPayPaymentRequestSessionSubmit.js +95 -0
  78. package/api/shopify/shopifyStorefrontSitemapGet.js +98 -0
  79. package/api/shopify/shopifyStorefrontUrlRedirectsGet.js +67 -0
  80. package/api/slack/docs.md +4 -0
  81. package/api/slack/slack.utils.js +36 -2
  82. package/api/slack/slackMessagePost.js +11 -3
  83. package/api/slack/slackMessagesSearch.js +81 -0
  84. package/api/utils.js +44 -8
  85. package/hosting/deployFromHostingYml.js +9 -21
  86. package/package.json +3 -1
  87. package/server.utils.js +24 -15
package/.creds.yml.sample CHANGED
@@ -31,6 +31,7 @@ github:
31
31
 
32
32
  slack:
33
33
  BOT_TOKEN: xoxb_________________________
34
+ USER_TOKEN: xoxp_________________________
34
35
 
35
36
  workable:
36
37
  BASE_URL: https://__.workable.com/spi/v3
@@ -2,6 +2,7 @@ const fs = require('fs').promises;
2
2
  const path = require('path');
3
3
  const { spawn } = require('child_process');
4
4
  const { askQuestion, capitaliseString } = require('../api/utils');
5
+ const choicy = require('choicy');
5
6
 
6
7
  const mineralRoot = path.join(__dirname, '..');
7
8
  const mineralApiDirectory = path.join(mineralRoot, 'api');
@@ -366,22 +367,17 @@ const selectDirInteractive = async (dirs) => {
366
367
  return '';
367
368
  }
368
369
 
369
- const dirIndex = await askQuestion(`Where does your new function live? \n${
370
+ const dir = await choicy(
370
371
  [
371
- '[0] api/ (root)',
372
- ...dirs.map((dir, index) => `[${ index + 1 }] ${ dir }`),
373
- ].join('\n')
374
- }\n`);
375
-
376
- if (String(dirIndex) === '0') {
377
- return '';
378
- }
379
-
380
- const dir = dirs[dirIndex - 1];
381
-
382
- if (!dir) {
383
- throw new Error(`${ dirIndex } not a valid option.`);
384
- }
372
+ { title: 'api/ (root)', value: '' },
373
+ ...dirs.map((dirName) => ({ title: dirName, value: dirName })),
374
+ ],
375
+ {
376
+ question: 'Where does your new function live?',
377
+ oneChoice: true,
378
+ index0: true,
379
+ },
380
+ );
385
381
 
386
382
  return dir;
387
383
  };
@@ -434,32 +430,18 @@ const selectTemplateInteractive = async ({ exampleFiles, dir, inMineral }) => {
434
430
  const templateScope = inMineral ? `in api/${ dir }` : 'from mineral';
435
431
  console.log(`\nFound ${ exampleFiles.length } template(s) ${ templateScope }:`);
436
432
 
437
- const templateIndex = await askQuestion(`Which template would you like to use? (press enter for _example.js) \n${
438
- exampleFiles.map((file, index) => {
439
- return `[${ index + 1 }] ${ formatTemplateLabel(file, { inMineral }) }`;
440
- }).join('\n')
441
- }\n`);
442
-
443
- if (!templateIndex || templateIndex.trim() === '') {
444
- const defaultFile = exampleFiles.find(file => (
445
- file.relativePath === '_example.js' || file.displayName === '_example.js'
446
- ));
447
-
448
- if (defaultFile) {
449
- return defaultFile.fullPath;
450
- }
451
-
452
- await fs.access(rootExampleJsPath);
453
- console.log('\nUsing template: _example.js');
454
- return rootExampleJsPath;
455
- }
456
-
457
- const selectedFile = exampleFiles[templateIndex - 1];
458
- if (!selectedFile) {
459
- throw new Error(`${ templateIndex } not a valid option.`);
460
- }
461
-
462
- return selectedFile.fullPath;
433
+ const selectedPath = await choicy(
434
+ exampleFiles.map((file) => ({
435
+ title: formatTemplateLabel(file, { inMineral }),
436
+ value: file.fullPath,
437
+ })),
438
+ {
439
+ question: 'Which template would you like to use?',
440
+ oneChoice: true,
441
+ },
442
+ );
443
+
444
+ return selectedPath;
463
445
  };
464
446
 
465
447
  const selectNameInteractive = async (dir) => {
package/api/UTILS.md CHANGED
@@ -45,7 +45,7 @@ Cheat sheet for [`utils.js`](utils.js). Prefer this over cold-grepping the file.
45
45
  | `surveyObject` | Summarise / compare object fields for assess steps. |
46
46
  | `diffObjects` | Field-level diffs between source and target. |
47
47
  | `logDeep` | Deep console dump (local inspect). |
48
- | `askQuestion` | Interactive confirm when `!HOSTED`. |
48
+ | `askQuestion` | Free-text prompt / inspect pause when `!HOSTED`. For discrete options, use npm `choicy` directly. |
49
49
 
50
50
  ## Small helpers often needed
51
51
 
@@ -1,7 +1,7 @@
1
1
  // https://app.swaggerhub.com/apis-docs/Bleckmann/warehousing/1.5.2#/PICKTICKET/getPickticketForId
2
2
 
3
3
  const { credsValidator } = require('../validators');
4
- const { ArgsWarden, objHasAny, oneFromManyInResponse } = require('../utils');
4
+ const { ArgsWarden, objHasAny, oneFromManyResponse } = require('../utils');
5
5
  const { bleckmannClient } = require('../bleckmann/bleckmann.utils');
6
6
  const { bleckmannPickticketsGet } = require('../bleckmann/bleckmannPickticketsGet');
7
7
 
@@ -54,7 +54,9 @@ const bleckmannPickticketGet = async (
54
54
  },
55
55
  );
56
56
 
57
- return oneFromManyInResponse(response, 'reference', pickticketReference);
57
+ return oneFromManyResponse(response, {
58
+ validator: i => i?.reference === pickticketReference,
59
+ });
58
60
  };
59
61
 
60
62
  const funcApiConfig = {
@@ -0,0 +1,139 @@
1
+ // https://docs.github.com/en/rest/actions/secrets#create-or-update-a-repository-secret
2
+ // https://docs.github.com/en/rest/actions/secrets#get-a-repository-public-key
3
+
4
+ const sodium = require('libsodium-wrappers');
5
+ const { ArgsWarden } = require('../utils');
6
+ const { credsValidator } = require('../validators');
7
+ const { githubClient } = require('../github/github.utils');
8
+ const { githubActionsSecretPublicKeyGet } = require('../github/githubActionsSecretPublicKeyGet');
9
+
10
+ const argsWarden = new ArgsWarden([
11
+ ['credsPayload', credsValidator],
12
+ ['owner'],
13
+ ['repo'],
14
+ ['secretName'],
15
+ ['secretValue'],
16
+ ]);
17
+
18
+ const encryptSecretValue = async (secretValue, publicKey) => {
19
+ await sodium.ready;
20
+
21
+ const binaryKey = sodium.from_base64(publicKey, sodium.base64_variants.ORIGINAL);
22
+ const binarySecret = sodium.from_string(String(secretValue));
23
+ const encryptedBytes = sodium.crypto_box_seal(binarySecret, binaryKey);
24
+
25
+ return sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL);
26
+ };
27
+
28
+ const githubActionsSecretCreateOrUpdate = async (
29
+ credsPayload,
30
+ owner,
31
+ repo,
32
+ secretName,
33
+ secretValue,
34
+ {
35
+ apiVersion,
36
+ keyId,
37
+ publicKey,
38
+ } = {},
39
+ ) => {
40
+
41
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
42
+ credsPayload,
43
+ owner,
44
+ repo,
45
+ secretName,
46
+ secretValue,
47
+ });
48
+ if (rejectResponse) {
49
+ return rejectResponse;
50
+ }
51
+
52
+ let resolvedKeyId = keyId;
53
+ let resolvedPublicKey = publicKey;
54
+
55
+ if (!resolvedKeyId || !resolvedPublicKey) {
56
+ const publicKeyResponse = await githubActionsSecretPublicKeyGet(
57
+ credsPayload,
58
+ owner,
59
+ repo,
60
+ { apiVersion },
61
+ );
62
+
63
+ if (!publicKeyResponse?.ok) {
64
+ return publicKeyResponse;
65
+ }
66
+
67
+ resolvedKeyId = publicKeyResponse.data.key_id;
68
+ resolvedPublicKey = publicKeyResponse.data.key;
69
+ }
70
+
71
+ const encryptedValue = await encryptSecretValue(secretValue, resolvedPublicKey);
72
+
73
+ const response = await githubClient.fetch({
74
+ requestPayload: {
75
+ method: 'put',
76
+ url: `/repos/${ owner }/${ repo }/actions/secrets/${ encodeURIComponent(secretName) }`,
77
+ body: {
78
+ encrypted_value: encryptedValue,
79
+ key_id: resolvedKeyId,
80
+ },
81
+ responseParser: async (res) => {
82
+ const text = await res.text();
83
+ if (!text) {
84
+ return {
85
+ secretName,
86
+ status: res.status,
87
+ };
88
+ }
89
+ try {
90
+ return JSON.parse(text);
91
+ } catch {
92
+ return {
93
+ secretName,
94
+ status: res.status,
95
+ body: text,
96
+ };
97
+ }
98
+ },
99
+ },
100
+ context: {
101
+ credsPayload,
102
+ apiVersion,
103
+ },
104
+ });
105
+
106
+ if (!response?.ok) {
107
+ return response;
108
+ }
109
+
110
+ return {
111
+ ok: true,
112
+ data: {
113
+ secretName,
114
+ ...(response.data || {}),
115
+ },
116
+ };
117
+ };
118
+
119
+ const funcApiConfig = {
120
+ argsWarden,
121
+ };
122
+
123
+ module.exports = {
124
+ githubActionsSecretCreateOrUpdate,
125
+ encryptSecretValue,
126
+ funcApiConfig,
127
+ };
128
+
129
+ /*
130
+ curl -X POST "http://localhost:8000/githubActionsSecretCreateOrUpdate" \
131
+ -H "Content-Type: application/json" \
132
+ -d '{
133
+ "credsPayload": { "credsPath": "github" },
134
+ "owner": "whitefoxboutique",
135
+ "repo": "whitefox-shopify-theme",
136
+ "secretName": "REGIONS",
137
+ "secretValue": "au,us,uk"
138
+ }'
139
+ */
@@ -0,0 +1,59 @@
1
+ // https://docs.github.com/en/rest/actions/secrets#get-a-repository-public-key
2
+
3
+ const { ArgsWarden } = require('../utils');
4
+ const { credsValidator } = require('../validators');
5
+ const { githubClient } = require('../github/github.utils');
6
+
7
+ const argsWarden = new ArgsWarden([
8
+ ['credsPayload', credsValidator],
9
+ ['owner'],
10
+ ['repo'],
11
+ ]);
12
+
13
+ const githubActionsSecretPublicKeyGet = async (
14
+ credsPayload,
15
+ owner,
16
+ repo,
17
+ {
18
+ apiVersion,
19
+ } = {},
20
+ ) => {
21
+
22
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
23
+ credsPayload,
24
+ owner,
25
+ repo,
26
+ });
27
+ if (rejectResponse) {
28
+ return rejectResponse;
29
+ }
30
+
31
+ return githubClient.fetch({
32
+ requestPayload: {
33
+ url: `/repos/${ owner }/${ repo }/actions/secrets/public-key`,
34
+ },
35
+ context: {
36
+ credsPayload,
37
+ apiVersion,
38
+ },
39
+ });
40
+ };
41
+
42
+ const funcApiConfig = {
43
+ argsWarden,
44
+ };
45
+
46
+ module.exports = {
47
+ githubActionsSecretPublicKeyGet,
48
+ funcApiConfig,
49
+ };
50
+
51
+ /*
52
+ curl -X POST "http://localhost:8000/githubActionsSecretPublicKeyGet" \
53
+ -H "Content-Type: application/json" \
54
+ -d '{
55
+ "credsPayload": { "credsPath": "github" },
56
+ "owner": "whitefoxboutique",
57
+ "repo": "whitefox-shopify-theme"
58
+ }'
59
+ */
@@ -17,6 +17,7 @@ const googlecalendarEventCreate = async (
17
17
  {
18
18
  subject,
19
19
  calendarId = 'primary',
20
+ sendUpdates = 'all',
20
21
  } = {},
21
22
  ) => {
22
23
 
@@ -36,6 +37,7 @@ const googlecalendarEventCreate = async (
36
37
 
37
38
  return googleApiCall(() => client.events.insert({
38
39
  calendarId,
40
+ sendUpdates,
39
41
  requestBody: eventData,
40
42
  }));
41
43
  };
@@ -1,6 +1,6 @@
1
1
  // https://linear.app/developers/graphql
2
2
 
3
- const { ArgsWarden, objHasAny, oneFromManyInResponse } = require('../utils');
3
+ const { ArgsWarden, objHasAny, oneFromManyResponse } = require('../utils');
4
4
  const { credsValidator } = require('../validators');
5
5
  const { linearClient } = require('../linear/linear.utils');
6
6
  const { linearTeamsGet } = require('./linearTeamsGet');
@@ -52,7 +52,10 @@ const linearTeamGet = async (
52
52
  const teamIdProp = teamName ? 'name' : 'key';
53
53
  const teamIdValue = teamName ? teamName : teamKey;
54
54
 
55
- const teamResponse = oneFromManyInResponse(teamsGetResponse, teamIdProp, teamIdValue);
55
+ const teamResponse = oneFromManyResponse(teamsGetResponse, {
56
+ validator: i => i[teamIdProp] === teamIdValue,
57
+ });
58
+
56
59
  if (teamResponse.ok && teamResponse.data) {
57
60
  teamId = teamResponse.data.id;
58
61
  }
@@ -109,6 +112,6 @@ curl -X POST "http://localhost:8000/linearTeamGet" \
109
112
  -H "Content-Type: application/json" \
110
113
  -d '{
111
114
  "credsPayload": { "credsPath": "linear" },
112
- "teamIdentifier": { "teamName": "Elite Four" }
115
+ "teamIdentifier": { "teamName": "WF Engineering" }
113
116
  }'
114
117
  */
@@ -4,4 +4,29 @@ module.exports = {
4
4
  DEFAULT_API_VERSION,
5
5
  MAX_PER_PAGE: 250,
6
6
  MAX_METAFIELDS_PER_SET: 25,
7
+ MAX_INVENTORY_QUANTITIES_PER_SET: 250,
8
+ INVENTORY_TYPES: [
9
+ 'available',
10
+ 'on_hand',
11
+ ],
12
+ // https://shopify.dev/docs/apps/build/orders-fulfillment/inventory-management-apps/manage-quantities-states#set-inventory-quantities-on-hand
13
+ INVENTORY_REASONS: [
14
+ 'correction',
15
+ 'cycle_count_available',
16
+ 'damaged',
17
+ 'movement_created',
18
+ 'movement_updated',
19
+ 'movement_received',
20
+ 'movement_canceled',
21
+ 'other',
22
+ 'promotion',
23
+ 'quality_control',
24
+ 'received',
25
+ 'reservation_created',
26
+ 'reservation_deleted',
27
+ 'reservation_updated',
28
+ 'restock',
29
+ 'safety_stock',
30
+ 'shrinkage',
31
+ ],
7
32
  };
@@ -18,14 +18,24 @@ const handleMutationUserErrors = async (state) => {
18
18
  return {};
19
19
  }
20
20
 
21
- const { userErrors, ...mutationResult } = mutationPayload;
21
+ const {
22
+ userErrors,
23
+ customerUserErrors,
24
+ ...mutationResult
25
+ } = mutationPayload;
26
+
27
+ // Storefront customer mutations use customerUserErrors; cart/admin use userErrors
28
+ const errors = [
29
+ ...Array.isArray(userErrors) ? userErrors : [],
30
+ ...Array.isArray(customerUserErrors) ? customerUserErrors : [],
31
+ ];
22
32
 
23
- if (!Array.isArray(userErrors)) {
33
+ if (!Array.isArray(userErrors) && !Array.isArray(customerUserErrors)) {
24
34
  return {};
25
35
  }
26
36
 
27
- if (userErrors.length) {
28
- const message = userErrors
37
+ if (errors.length) {
38
+ const message = errors
29
39
  .map((userError) => userError?.message)
30
40
  .filter(Boolean)
31
41
  .join('; ');
@@ -36,7 +46,7 @@ const handleMutationUserErrors = async (state) => {
36
46
  error: {
37
47
  code: 'USER_ERROR',
38
48
  message: message || 'Mutation failed',
39
- details: userErrors,
49
+ details: errors,
40
50
  },
41
51
  ...(Object.keys(mutationResult).length ? { data: mutationResult } : {}),
42
52
  },
@@ -51,6 +61,26 @@ const handleMutationUserErrors = async (state) => {
51
61
  };
52
62
  };
53
63
 
64
+ const buildInContextDirective = (inContext) => {
65
+ if (!inContext || typeof inContext !== 'object') {
66
+ return '';
67
+ }
68
+
69
+ const parts = [];
70
+ if (inContext.country) {
71
+ parts.push(`country: ${ inContext.country }`);
72
+ }
73
+ if (inContext.language) {
74
+ parts.push(`language: ${ inContext.language }`);
75
+ }
76
+
77
+ if (!parts.length) {
78
+ return '';
79
+ }
80
+
81
+ return ` @inContext(${ parts.join(', ') })`;
82
+ };
83
+
54
84
  const useUrlAndAuthHeaders = async (state) => {
55
85
  const { requestPayload, context } = state;
56
86
  const {
@@ -187,7 +217,11 @@ const parseShopifyJsonl = (jsonl) => {
187
217
  .map((word) => word[0].toUpperCase() + word.slice(1))
188
218
  .join('');
189
219
  nestedParentType = sentenceCaseString(nestedParentType);
190
- parentObject = parentObject[nestedParentType];
220
+ // Only dig into a nested field when it exists on the parent. InventoryLevel
221
+ // GIDs use ?inventory_item_id= for uniqueness; __parentId is already the item.
222
+ if (parentObject[nestedParentType]) {
223
+ parentObject = parentObject[nestedParentType];
224
+ }
191
225
  }
192
226
 
193
227
  if (!parentObject) {
@@ -252,4 +286,5 @@ module.exports = {
252
286
  shopifyClient,
253
287
  shopifyStorefrontClient,
254
288
  shopifyProductFlattenMetafields,
289
+ buildInContextDirective,
255
290
  };
@@ -0,0 +1,164 @@
1
+ // https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventorySetQuantities
2
+
3
+ const { credsValidator } = require('../validators');
4
+ const {
5
+ ArgsWarden,
6
+ arrayToChunks,
7
+ actionSingleOrMultiple,
8
+ objHasAll,
9
+ valueProvided,
10
+ } = require('../utils');
11
+ const { shopifyMutationDo } = require('../shopify/shopifyMutationDo');
12
+ const {
13
+ MAX_INVENTORY_QUANTITIES_PER_SET,
14
+ INVENTORY_TYPES,
15
+ INVENTORY_REASONS,
16
+ } = require('../shopify/shopify.constants');
17
+
18
+ const defaultInventoryAdjustmentGroupReturnAttrs = `
19
+ createdAt
20
+ reason
21
+ changes {
22
+ name
23
+ delta
24
+ quantityAfterChange
25
+ item {
26
+ sku
27
+ }
28
+ }
29
+ `;
30
+
31
+ const inventoryQuantityInputValidator = (p) => {
32
+ return objHasAll(p, ['inventoryItemId', 'locationId', 'quantity']);
33
+ };
34
+
35
+ const argsWarden = new ArgsWarden([
36
+ ['credsPayload', credsValidator],
37
+ ['inventoryType', p => INVENTORY_TYPES.includes(p)],
38
+ ['quantities', p => Array.isArray(p) && p.length > 0 && p.every(inventoryQuantityInputValidator)],
39
+ ['reason', p => INVENTORY_REASONS.includes(p)],
40
+ ['idempotencyKey'],
41
+ ]);
42
+
43
+ const shopifyInventoryQuantitiesSetChunk = async (
44
+ credsPayload,
45
+ inventoryType,
46
+ quantities,
47
+ reason,
48
+ idempotencyKey,
49
+ {
50
+ apiVersion,
51
+ inventoryAdjustmentGroupReturnAttrs = defaultInventoryAdjustmentGroupReturnAttrs,
52
+ referenceDocumentUri,
53
+ } = {},
54
+ ) => {
55
+
56
+ const quantitiesForInput = quantities.map(({
57
+ inventoryItemId,
58
+ locationId,
59
+ quantity,
60
+ changeFromQuantity = null,
61
+ }) => ({
62
+ inventoryItemId: `gid://shopify/InventoryItem/${ inventoryItemId }`,
63
+ locationId: `gid://shopify/Location/${ locationId }`,
64
+ quantity,
65
+ changeFromQuantity,
66
+ }));
67
+
68
+ return shopifyMutationDo(
69
+ credsPayload,
70
+ 'inventorySetQuantities',
71
+ {
72
+ mutationVariables: {
73
+ input: {
74
+ type: 'InventorySetQuantitiesInput!',
75
+ value: {
76
+ name: inventoryType,
77
+ reason,
78
+ quantities: quantitiesForInput,
79
+ ...(valueProvided(referenceDocumentUri) && { referenceDocumentUri }),
80
+ },
81
+ },
82
+ },
83
+ returnSchema: `inventoryAdjustmentGroup { ${ inventoryAdjustmentGroupReturnAttrs } }`,
84
+ apiVersion,
85
+ idempotencyKey,
86
+ },
87
+ );
88
+ };
89
+
90
+ const shopifyInventoryQuantitiesSet = async (
91
+ credsPayload,
92
+ inventoryType,
93
+ quantities,
94
+ reason,
95
+ idempotencyKey,
96
+ {
97
+ queueRunOptions,
98
+ apiVersion,
99
+ inventoryAdjustmentGroupReturnAttrs = defaultInventoryAdjustmentGroupReturnAttrs,
100
+ referenceDocumentUri,
101
+ } = {},
102
+ ) => {
103
+
104
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
105
+ credsPayload,
106
+ inventoryType,
107
+ quantities,
108
+ reason,
109
+ idempotencyKey,
110
+ });
111
+ if (rejectResponse) {
112
+ return rejectResponse;
113
+ }
114
+
115
+ const chunks = arrayToChunks(quantities, MAX_INVENTORY_QUANTITIES_PER_SET);
116
+
117
+ return actionSingleOrMultiple(
118
+ chunks,
119
+ shopifyInventoryQuantitiesSetChunk,
120
+ (chunk) => ({
121
+ args: [
122
+ credsPayload,
123
+ inventoryType,
124
+ chunk,
125
+ reason,
126
+ chunks.length === 1 ? idempotencyKey : `${ idempotencyKey }:${ chunks.indexOf(chunk) }`,
127
+ {
128
+ apiVersion,
129
+ inventoryAdjustmentGroupReturnAttrs,
130
+ referenceDocumentUri,
131
+ },
132
+ ],
133
+ }),
134
+ {
135
+ ...(queueRunOptions ? { queueRunOptions } : {}),
136
+ },
137
+ );
138
+ };
139
+
140
+ const funcApiConfig = {
141
+ argsWarden,
142
+ };
143
+
144
+ module.exports = {
145
+ shopifyInventoryQuantitiesSet,
146
+ funcApiConfig,
147
+ };
148
+
149
+ /*
150
+ curl -X POST "http://localhost:8000/shopifyInventoryQuantitiesSet" \
151
+ -H "Content-Type: application/json" \
152
+ -d '{
153
+ "credsPayload": { "credsPath": "shopify.wakanda" },
154
+ "inventoryType": "on_hand",
155
+ "quantities": [{
156
+ "inventoryItemId": "1234567890",
157
+ "locationId": "1234567890",
158
+ "changeFromQuantity": 10,
159
+ "quantity": 5
160
+ }],
161
+ "reason": "correction",
162
+ "idempotencyKey": "..."
163
+ }'
164
+ */
@@ -14,6 +14,7 @@ const shopifyMutationDo = async (
14
14
  mutationVariables = {},
15
15
  returnSchema = '',
16
16
  apiVersion,
17
+ idempotencyKey,
17
18
  ...clientOptions
18
19
  } = {},
19
20
  ) => {
@@ -30,9 +31,15 @@ const shopifyMutationDo = async (
30
31
  returnSchema += ' userErrors { field message }';
31
32
  }
32
33
 
34
+ const argumentEntries = Object.entries(mutationVariables);
35
+ const variableEntries = [
36
+ ...argumentEntries,
37
+ ...idempotencyKey ? [['idempotencyKey', { type: 'String!', value: idempotencyKey }]] : [],
38
+ ];
39
+
33
40
  const mutation = `
34
- mutation ${ mutationName }(${ Object.entries(mutationVariables).map(([name, { type }]) => `$${ name }: ${ type }`).join(', ') }) {
35
- ${ mutationName }(${ Object.keys(mutationVariables).map(name => `${ name }: $${ name }`).join(', ') }) {
41
+ mutation ${ mutationName }(${ variableEntries.map(([name, { type }]) => `$${ name }: ${ type }`).join(', ') }) {
42
+ ${ mutationName }(${ argumentEntries.map(([name]) => `${ name }: $${ name }`).join(', ') })${ idempotencyKey ? ' @idempotent(key: $idempotencyKey)' : '' } {
36
43
  ${ returnSchema }
37
44
  userErrors {
38
45
  field
@@ -42,10 +49,9 @@ const shopifyMutationDo = async (
42
49
  }
43
50
  `;
44
51
 
45
- const variables = {};
46
- for (const [name, { value }] of Object.entries(mutationVariables)) {
47
- variables[name] = value;
48
- }
52
+ const variables = Object.fromEntries(
53
+ variableEntries.map(([name, { value }]) => [name, value]),
54
+ );
49
55
 
50
56
  const response = await shopifyClient.fetch({
51
57
  requestPayload: {