@foxtware/mineral 1.0.4 → 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.
- package/.creds.yml.sample +1 -0
- package/_build_scripts/createNewFunction.js +23 -41
- package/api/UTILS.md +1 -1
- package/api/bleckmann/bleckmannPickticketGet.js +4 -2
- package/api/github/githubActionsSecretCreateOrUpdate.js +139 -0
- package/api/github/githubActionsSecretPublicKeyGet.js +59 -0
- package/api/google/googlecalendarEventCreate.js +2 -0
- package/api/linear/linearTeamGet.js +6 -3
- package/api/shopify/shopify.constants.js +25 -0
- package/api/shopify/shopify.utils.js +41 -6
- package/api/shopify/shopifyInventoryQuantitiesSet.js +164 -0
- package/api/shopify/shopifyMutationDo.js +12 -6
- package/api/shopify/shopifyShopGet.js +68 -0
- package/api/shopify/shopifyStorefrontArticleGet.js +86 -0
- package/api/shopify/shopifyStorefrontArticlesGet.js +67 -0
- package/api/shopify/shopifyStorefrontBlogGet.js +110 -0
- package/api/shopify/shopifyStorefrontBlogsGet.js +66 -0
- package/api/shopify/shopifyStorefrontCartAttributesUpdate.js +87 -0
- package/api/shopify/shopifyStorefrontCartBuyerIdentityUpdate.js +87 -0
- package/api/shopify/shopifyStorefrontCartCreate.js +83 -0
- package/api/shopify/shopifyStorefrontCartDeliveryAddressesAdd.js +87 -0
- package/api/shopify/shopifyStorefrontCartDeliveryAddressesRemove.js +87 -0
- package/api/shopify/shopifyStorefrontCartDeliveryAddressesReplace.js +87 -0
- package/api/shopify/shopifyStorefrontCartDeliveryAddressesUpdate.js +87 -0
- package/api/shopify/shopifyStorefrontCartDiscountCodesUpdate.js +87 -0
- package/api/shopify/shopifyStorefrontCartGet.js +100 -0
- package/api/shopify/shopifyStorefrontCartGiftCardCodesAdd.js +87 -0
- package/api/shopify/shopifyStorefrontCartGiftCardCodesRemove.js +87 -0
- package/api/shopify/shopifyStorefrontCartGiftCardCodesUpdate.js +87 -0
- package/api/shopify/shopifyStorefrontCartLinesAdd.js +87 -0
- package/api/shopify/shopifyStorefrontCartLinesRemove.js +87 -0
- package/api/shopify/shopifyStorefrontCartLinesUpdate.js +87 -0
- package/api/shopify/shopifyStorefrontCartMetafieldDelete.js +69 -0
- package/api/shopify/shopifyStorefrontCartMetafieldsSet.js +80 -0
- package/api/shopify/shopifyStorefrontCartNoteUpdate.js +87 -0
- package/api/shopify/shopifyStorefrontCartSelectedDeliveryOptionsUpdate.js +87 -0
- package/api/shopify/shopifyStorefrontCollectionGet.js +92 -0
- package/api/shopify/shopifyStorefrontCollectionsGet.js +66 -0
- package/api/shopify/shopifyStorefrontCustomerAccessTokenCreate.js +75 -0
- package/api/shopify/shopifyStorefrontCustomerAccessTokenCreateWithMultipass.js +72 -0
- package/api/shopify/shopifyStorefrontCustomerAccessTokenDelete.js +67 -0
- package/api/shopify/shopifyStorefrontCustomerAccessTokenRenew.js +72 -0
- package/api/shopify/shopifyStorefrontCustomerActivate.js +98 -0
- package/api/shopify/shopifyStorefrontCustomerActivateByUrl.js +91 -0
- package/api/shopify/shopifyStorefrontCustomerAddressCreate.js +94 -0
- package/api/shopify/shopifyStorefrontCustomerAddressDelete.js +78 -0
- package/api/shopify/shopifyStorefrontCustomerAddressUpdate.js +101 -0
- package/api/shopify/shopifyStorefrontCustomerCreate.js +79 -0
- package/api/shopify/shopifyStorefrontCustomerDefaultAddressUpdate.js +89 -0
- package/api/shopify/shopifyStorefrontCustomerGet.js +92 -0
- package/api/shopify/shopifyStorefrontCustomerRecover.js +64 -0
- package/api/shopify/shopifyStorefrontCustomerReset.js +98 -0
- package/api/shopify/shopifyStorefrontCustomerResetByUrl.js +91 -0
- package/api/shopify/shopifyStorefrontCustomerUpdate.js +93 -0
- package/api/shopify/shopifyStorefrontGet.js +233 -0
- package/api/shopify/shopifyStorefrontLocalizationGet.js +98 -0
- package/api/shopify/shopifyStorefrontLocationsGet.js +70 -0
- package/api/shopify/shopifyStorefrontMenuGet.js +93 -0
- package/api/shopify/shopifyStorefrontMetaobjectGet.js +126 -0
- package/api/shopify/shopifyStorefrontMetaobjectsGet.js +73 -0
- package/api/shopify/shopifyStorefrontMutationDo.js +103 -0
- package/api/shopify/shopifyStorefrontNodeGet.js +90 -0
- package/api/shopify/shopifyStorefrontNodesGet.js +86 -0
- package/api/shopify/shopifyStorefrontPageGet.js +93 -0
- package/api/shopify/shopifyStorefrontPagesGet.js +66 -0
- package/api/shopify/shopifyStorefrontPaymentSettingsGet.js +79 -0
- package/api/shopify/shopifyStorefrontPredictiveSearch.js +138 -0
- package/api/shopify/shopifyStorefrontProductGet.js +94 -0
- package/api/shopify/shopifyStorefrontProductRecommendationsGet.js +112 -0
- package/api/shopify/shopifyStorefrontProductTagsGet.js +80 -0
- package/api/shopify/shopifyStorefrontProductTypesGet.js +80 -0
- package/api/shopify/shopifyStorefrontProductsGet.js +76 -0
- package/api/shopify/shopifyStorefrontPublicApiVersionsGet.js +75 -0
- package/api/shopify/shopifyStorefrontSearch.js +8 -2
- package/api/shopify/shopifyStorefrontShopGet.js +81 -0
- package/api/shopify/shopifyStorefrontShopPayPaymentRequestSessionCreate.js +91 -0
- package/api/shopify/shopifyStorefrontShopPayPaymentRequestSessionSubmit.js +95 -0
- package/api/shopify/shopifyStorefrontSitemapGet.js +98 -0
- package/api/shopify/shopifyStorefrontUrlRedirectsGet.js +67 -0
- package/api/slack/docs.md +4 -0
- package/api/slack/slack.utils.js +36 -2
- package/api/slack/slackMessagePost.js +11 -3
- package/api/slack/slackMessagesSearch.js +81 -0
- package/api/utils.js +47 -10
- package/hosting/deployFromHostingYml.js +9 -21
- package/package.json +3 -1
- package/server.utils.js +24 -15
package/.creds.yml.sample
CHANGED
|
@@ -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
|
|
370
|
+
const dir = await choicy(
|
|
370
371
|
[
|
|
371
|
-
'
|
|
372
|
-
...dirs.map((
|
|
373
|
-
]
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
|
438
|
-
exampleFiles.map((file
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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` |
|
|
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,
|
|
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
|
|
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,
|
|
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 =
|
|
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": "
|
|
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 {
|
|
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 (
|
|
28
|
-
const message =
|
|
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:
|
|
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
|
-
|
|
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 }(${
|
|
35
|
-
${ mutationName }(${
|
|
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
|
-
|
|
47
|
-
|
|
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: {
|