@foxtware/mineral 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.creds.yml.sample +21 -0
  2. package/AGENTS.md +13 -0
  3. package/README.md +52 -0
  4. package/_build_scripts/copyCredsToEnv.js +45 -0
  5. package/_build_scripts/createNewFunction.js +361 -0
  6. package/_docs/standard_response_examples.md +67 -0
  7. package/api/_example.js +45 -0
  8. package/api/constants.js +3 -0
  9. package/api/linear/linear.constants.js +5 -0
  10. package/api/linear/linear.utils.js +43 -0
  11. package/api/linear/linearIssuesGet.js +106 -0
  12. package/api/peoplevox/.gitkeep +0 -0
  13. package/api/peoplevox/_example.js +49 -0
  14. package/api/peoplevox/peoplevox.constants.js +3 -0
  15. package/api/peoplevox/peoplevox.utils.js +191 -0
  16. package/api/peoplevox/peoplevoxAuthGet.js +99 -0
  17. package/api/peoplevox/peoplevoxGetSingle.js +119 -0
  18. package/api/peoplevox/peoplevoxOrderEdit.js +82 -0
  19. package/api/peoplevox/peoplevoxOrderGet.js +60 -0
  20. package/api/peoplevox/peoplevoxReportGet.js +76 -0
  21. package/api/shopify/.gitkeep +0 -0
  22. package/api/shopify/_example.create.js +69 -0
  23. package/api/shopify/_example.getsingle.js +55 -0
  24. package/api/shopify/_example.js +45 -0
  25. package/api/shopify/_example.mutation.js +58 -0
  26. package/api/shopify/shopify.constants.js +7 -0
  27. package/api/shopify/shopify.utils.js +123 -0
  28. package/api/shopify/shopifyCollectionGet.js +109 -0
  29. package/api/shopify/shopifyCustomerCreate.js +71 -0
  30. package/api/shopify/shopifyCustomerGet.js +112 -0
  31. package/api/shopify/shopifyCustomerMarketingConsentUpdateEmail.js +84 -0
  32. package/api/shopify/shopifyCustomerUpdate.js +75 -0
  33. package/api/shopify/shopifyCustomersGet.js +50 -0
  34. package/api/shopify/shopifyGet.js +309 -0
  35. package/api/shopify/shopifyGetSingle.js +106 -0
  36. package/api/shopify/shopifyGiftCardCreate.js +95 -0
  37. package/api/shopify/shopifyGiftCardDeactivate.js +58 -0
  38. package/api/shopify/shopifyMetafieldsSet.js +100 -0
  39. package/api/shopify/shopifyMutationDo.js +95 -0
  40. package/api/shopify/shopifyOrderGet.js +123 -0
  41. package/api/shopify/shopifyOrdersGet.js +80 -0
  42. package/api/shopify/shopifyPageDelete.js +59 -0
  43. package/api/shopify/shopifyPageGet.js +57 -0
  44. package/api/shopify/shopifyPageUpdate.js +79 -0
  45. package/api/shopify/shopifyTagsAdd.js +94 -0
  46. package/api/shopify/shopifyTagsRemove.js +94 -0
  47. package/api/shopify/shopifyThemeDelete.js +87 -0
  48. package/api/shopify/shopifyThemeDuplicate.js +70 -0
  49. package/api/shopify/shopifyThemeGet.js +59 -0
  50. package/api/shopify/shopifyThemeUpdate.js +71 -0
  51. package/api/shopify/shopifyThemesGet.js +63 -0
  52. package/api/utils.js +1166 -0
  53. package/api/validators.js +12 -0
  54. package/api/yotpo/yotpo.constants.js +7 -0
  55. package/api/yotpo/yotpo.utils.js +47 -0
  56. package/api/yotpo/yotpoCampaignsGet.js +75 -0
  57. package/api/yotpo/yotpoCustomerActionRecord.js +95 -0
  58. package/api/yotpo/yotpoCustomerAnniversaryGet.js +60 -0
  59. package/api/yotpo/yotpoCustomerAnniversarySet.js +78 -0
  60. package/api/yotpo/yotpoCustomerBirthdaySet.js +91 -0
  61. package/api/yotpo/yotpoCustomerGet.js +104 -0
  62. package/api/yotpo/yotpoCustomerPointsAdjust.js +89 -0
  63. package/api/yotpo/yotpoCustomerUpsert.js +91 -0
  64. package/api/yotpo/yotpoCustomersRecentGet.js +62 -0
  65. package/api/yotpo/yotpoRedemptionCreate.js +96 -0
  66. package/api/yotpo/yotpoRedemptionOptionsGet.js +78 -0
  67. package/api/yotpo/yotpoVipTiersGet.js +51 -0
  68. package/api/youtube/youtubeChannelVideosGet.js +249 -0
  69. package/bin/mineral.js +5 -0
  70. package/package.json +24 -0
  71. package/server.js +196 -0
  72. package/server.utils.js +228 -0
  73. package/test.js +19 -0
package/api/utils.js ADDED
@@ -0,0 +1,1166 @@
1
+ require('dotenv').config();
2
+
3
+ const xml2js = require('xml2js');
4
+ const readline = require('readline');
5
+ const fs = require('fs').promises;
6
+ const yaml = require('yaml');
7
+ const { EventEmitter } = require('events');
8
+
9
+ const { HOSTED } = require('./constants');
10
+
11
+ const wait = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms));
12
+
13
+ const objHasAny = (obj, keys) => {
14
+ return keys.some((key) => obj[key] !== undefined);
15
+ };
16
+
17
+ const capitaliseString = (string) => `${ string[0].toUpperCase() }${ string.slice(1) }`;
18
+ const sentenceCaseString = (string) => `${ string[0].toLowerCase() }${ string.slice(1) }`;
19
+
20
+ const credsByPath = (credsPath, credsObject) => {
21
+ const pathNodes = pathAsArray(credsPath);
22
+
23
+ let creds = {};
24
+ let credsEdge;
25
+
26
+ for (const node of pathNodes) {
27
+
28
+ credsEdge = (credsEdge ? credsEdge : credsObject)[node];
29
+
30
+ if (!credsEdge) {
31
+ return creds;
32
+ }
33
+
34
+ const nonStructuralCreds = Object.fromEntries(
35
+ Object.entries(credsEdge).filter(([key]) => {
36
+ return key === key.toUpperCase();
37
+ })
38
+ );
39
+
40
+ creds = {
41
+ ...creds,
42
+ ...nonStructuralCreds,
43
+ };
44
+ }
45
+
46
+ return creds;
47
+ };
48
+
49
+ const credsFromPayload = async (credsPayload) => {
50
+ const {
51
+ credsPath,
52
+ credsObject,
53
+ credsProvider,
54
+ } = credsPayload;
55
+
56
+ if (credsObject) {
57
+ return credsObject;
58
+ }
59
+
60
+ if (credsPath) {
61
+ let credsYmlAsObject;
62
+
63
+ if (HOSTED) {
64
+ const { CREDS } = process.env;
65
+
66
+ if (!CREDS) {
67
+ throw new Error('HOSTED mode requires CREDS environment variable');
68
+ }
69
+
70
+ try {
71
+ credsYmlAsObject = JSON.parse(CREDS);
72
+ } catch (err) {
73
+ throw new Error(`Malformed CREDS environment variable: ${ err }`);
74
+ }
75
+ } else {
76
+ const credsText = await fs.readFile('.creds.yml', 'utf8');
77
+ credsYmlAsObject = yaml.parse(credsText);
78
+ }
79
+
80
+ return credsByPath(credsPath, credsYmlAsObject);
81
+ }
82
+
83
+ return false;
84
+ };
85
+
86
+ const xmlResponseParser = (parserOptions = {}) => {
87
+ const parser = new xml2js.Parser({
88
+ explicitArray: false,
89
+ mergeAttrs: true,
90
+ ignoreAttrs: true,
91
+ ...parserOptions,
92
+ });
93
+
94
+ return async (response) => {
95
+ const text = await response.text();
96
+ return parser.parseStringPromise(text);
97
+ };
98
+ };
99
+
100
+ const getResponseParser = (contentType) => {
101
+ if (contentType.includes('application/json')) return (res) => res.json();
102
+ if (contentType.includes('text/xml')) return xmlResponseParser();
103
+ if (contentType.includes('application/soap+xml')) return (res) => res.text();
104
+ if (contentType.includes('application/octet-stream')) return (res) => res.arrayBuffer();
105
+ return (res) => res.text(); // safe default
106
+ };
107
+
108
+ const customFetch = async (url, {
109
+ method = 'get',
110
+ headers = {},
111
+ params,
112
+ body,
113
+ responseParser,
114
+
115
+ verbose,
116
+ omitRequestId = false,
117
+ } = {}) => {
118
+
119
+ if (!omitRequestId && !headers['x-request-id']) {
120
+ headers['x-request-id'] = String(Date.now());
121
+ }
122
+
123
+ if (body && !headers['Content-Type']) {
124
+ headers['Content-Type'] = 'application/json';
125
+ }
126
+
127
+ if (params) {
128
+ const search = new URLSearchParams(params);
129
+ url += (url.includes('?') ? '&' : '?') + search.toString();
130
+ }
131
+
132
+ let cooldown = 3000;
133
+ let retryAttempt = 0;
134
+ const maxRetries = 5;
135
+ const retryStatuses = new Set([408, 429, 500, 502, 503, 504]);
136
+
137
+ while (true) {
138
+ try {
139
+ const response = await fetch(url, {
140
+ method,
141
+ headers,
142
+ ...body
143
+ ? { body: typeof body === 'string'
144
+ ? body
145
+ : JSON.stringify(body)
146
+ } : {},
147
+ });
148
+
149
+ const responseContentType = response.headers.get('content-type');
150
+ console.log(responseContentType);
151
+
152
+ if (!responseParser) {
153
+ responseParser = getResponseParser(responseContentType);
154
+ }
155
+
156
+ const parsedResponse = await responseParser(response);
157
+ logDeep({ parsedResponse });
158
+
159
+ if (response.ok) {
160
+ return {
161
+ ok: true,
162
+ data: parsedResponse,
163
+ };
164
+ }
165
+
166
+ const { status } = response;
167
+ verbose && console.error(status, parsedResponse);
168
+
169
+ if (retryStatuses.has(status)) {
170
+ if (retryAttempt >= maxRetries) {
171
+ console.log('Ran out of retries');
172
+ return {
173
+ ok: false,
174
+ error: {
175
+ code: 'RETRIES_EXHAUSTED',
176
+ details: parsedResponse,
177
+ },
178
+ };
179
+ }
180
+
181
+ retryAttempt++;
182
+ const retryAfter = response.headers.get('retry-after');
183
+ const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : cooldown;
184
+ verbose && console.log(`Retry attempt #${ retryAttempt }, waiting ${ waitTime }`);
185
+ await wait(waitTime);
186
+ cooldown += cooldown;
187
+ continue;
188
+ }
189
+
190
+ return {
191
+ ok: false,
192
+ error: {
193
+ details: parsedResponse,
194
+ },
195
+ };
196
+
197
+ } catch (error) {
198
+ verbose && console.error(error);
199
+ return {
200
+ ok: false,
201
+ error: {
202
+ details: error,
203
+ },
204
+ };
205
+ }
206
+ }
207
+ };
208
+
209
+ const logDeep = (...args) => {
210
+ for (const arg of args) {
211
+ console.dir(arg, { depth: null });
212
+ }
213
+ };
214
+
215
+ const askQuestion = async (
216
+ query,
217
+ {
218
+ defaultAnswer,
219
+ validate,
220
+ invalidMessage = 'Invalid input, try again.',
221
+ } = {},
222
+ ) => {
223
+ const prompt = defaultAnswer !== undefined
224
+ ? `${ query }(default: ${ defaultAnswer }) `
225
+ : query;
226
+
227
+ while (true) {
228
+ const answer = await new Promise((resolve, reject) => {
229
+ const rl = readline.createInterface({
230
+ input: process.stdin,
231
+ output: process.stdout,
232
+ });
233
+
234
+ let settled = false;
235
+
236
+ const finish = (value) => {
237
+ if (settled) return;
238
+ settled = true;
239
+ rl.close();
240
+ resolve(value);
241
+ };
242
+
243
+ rl.on('close', () => {
244
+ if (settled) return;
245
+ settled = true;
246
+ if (defaultAnswer !== undefined) {
247
+ resolve(defaultAnswer);
248
+ } else {
249
+ reject(new Error('Input closed'));
250
+ }
251
+ });
252
+
253
+ rl.question(prompt, (response) => {
254
+ finish(response === '' && defaultAnswer !== undefined ? defaultAnswer : response);
255
+ });
256
+ });
257
+
258
+ if (!validate || validate(answer)) {
259
+ return answer;
260
+ }
261
+
262
+ console.log(invalidMessage);
263
+ }
264
+ };
265
+
266
+ const pathAsArray = (path) => {
267
+ let nodes = Array.isArray(path)
268
+ ? path.map(node => {
269
+ try {
270
+ return node.split('.');
271
+ } catch (err) {
272
+ return null;
273
+ }
274
+ })
275
+ : path.split('.');
276
+ nodes = nodes.flat().filter(n => n);
277
+ return nodes;
278
+ };
279
+
280
+ const objectDigNodeAtPath = (obj, path) => {
281
+ let nodes = pathAsArray(path);
282
+ console.log(nodes);
283
+
284
+ let output = obj;
285
+ for (const node of nodes) {
286
+ if (output == null) {
287
+ return undefined;
288
+ }
289
+ output = output[node];
290
+ }
291
+ return output;
292
+ };
293
+
294
+ const appendUrlToBase = (baseUrl, url) => {
295
+ if (!baseUrl) {
296
+ return url;
297
+ }
298
+
299
+ if (!url) {
300
+ return baseUrl;
301
+ }
302
+
303
+ // Remove baseUrl from url if it's there
304
+ if (url.startsWith(baseUrl)) {
305
+ url = url.slice(baseUrl.length);
306
+ }
307
+
308
+ // Remove trailing and leading slashes
309
+ baseUrl = baseUrl.replace(/\/$/, '');
310
+ url = url.replace(/^\//, '');
311
+
312
+ return `${ baseUrl }/${ url }`;
313
+ };
314
+
315
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
316
+
317
+ const stripGraphqlEdgesAndNodes = (value) => {
318
+ if (Array.isArray(value)) {
319
+ return value.map(stripGraphqlEdgesAndNodes);
320
+ }
321
+
322
+ if (!isObject(value)) {
323
+ return value;
324
+ }
325
+
326
+ if (Array.isArray(value.edges)) {
327
+ return value.edges.map((edge) => stripGraphqlEdgesAndNodes(edge?.node));
328
+ }
329
+
330
+ const output = {};
331
+ for (const [key, nestedValue] of Object.entries(value)) {
332
+ output[key] = stripGraphqlEdgesAndNodes(nestedValue);
333
+ }
334
+ return output;
335
+ };
336
+
337
+ const collapseDataOnlyWrappers = (value) => {
338
+ if (Array.isArray(value)) {
339
+ return value.map(collapseDataOnlyWrappers);
340
+ }
341
+
342
+ if (!isObject(value)) {
343
+ return value;
344
+ }
345
+
346
+ const keys = Object.keys(value);
347
+ if (keys.length === 1 && keys[0] === 'data') {
348
+ return collapseDataOnlyWrappers(value.data);
349
+ }
350
+
351
+ const output = {};
352
+ for (const [key, nestedValue] of Object.entries(value)) {
353
+ output[key] = collapseDataOnlyWrappers(nestedValue);
354
+ }
355
+ return output;
356
+ };
357
+
358
+ const objectMergeEntries = (object, updates) => {
359
+ const output = { ...object };
360
+
361
+ for (const [key, value] of Object.entries(updates)) {
362
+ if (value === null) {
363
+ delete output[key];
364
+ continue;
365
+ }
366
+
367
+ if (!isObject(value)) {
368
+ output[key] = value;
369
+ continue;
370
+ }
371
+
372
+ output[key] = {
373
+ ...(output[key] ?? {}),
374
+ ...value,
375
+ };
376
+ }
377
+
378
+ return output;
379
+ };
380
+
381
+ class Chain {
382
+ constructor(steps = [], { outputHandler = objectMergeEntries } = {}) {
383
+ this.steps = steps;
384
+ this.outputHandler = outputHandler;
385
+ }
386
+
387
+ addSteps(steps) {
388
+ this.steps.push(...steps);
389
+ return this;
390
+ }
391
+
392
+ getSteps() {
393
+ return this.steps;
394
+ }
395
+
396
+ async run(state, { inspect = false, outputHandler = this.outputHandler } = {}) {
397
+ for (const step of this.steps) {
398
+ const updates = await step(state) || {};
399
+
400
+ inspect && logDeep({ updates });
401
+ inspect && await askQuestion('?');
402
+
403
+ state = outputHandler(state, updates);
404
+
405
+ inspect && logDeep({ state });
406
+ inspect && await askQuestion('?');
407
+
408
+ if (updates.breakChain) {
409
+ return state;
410
+ }
411
+ }
412
+
413
+ return state;
414
+ }
415
+ }
416
+
417
+ class FetchClient {
418
+ constructor({
419
+ context, // necessary data for preparer and interpreters to refer to
420
+ requestPreparer, // a function that updates requests before sending
421
+ responseInterpreter, // a function that transforms responses to report back
422
+ } = {}) {
423
+ this.context = context;
424
+ this.requestPreparer = requestPreparer; // can be a Chain
425
+ this.responseInterpreter = responseInterpreter; // can be a Chain
426
+ }
427
+
428
+ async fetch({
429
+ url,
430
+
431
+ // customFetch payload
432
+ method,
433
+ headers,
434
+ params,
435
+ body,
436
+
437
+ responseInterpreter,
438
+
439
+ context = {},
440
+ inspect = false,
441
+ } = {}) {
442
+
443
+ let mergedContext = {
444
+ ...(this.context ?? {}),
445
+ ...(context ?? {}),
446
+ };
447
+ inspect && logDeep({ mergedContext });
448
+ inspect && await askQuestion('?');
449
+
450
+ let requestPayload = {
451
+ url,
452
+ ...(method ? { method } : {}),
453
+ ...(headers ? { headers } : {}),
454
+ ...(params ? { params } : {}),
455
+ ...(body ? { body } : {}),
456
+ };
457
+ inspect && logDeep({ requestPayload });
458
+ inspect && await askQuestion('?');
459
+
460
+ if (this.requestPreparer) {
461
+ if (this.requestPreparer.run) {
462
+ ({ requestPayload, context: mergedContext } = await this.requestPreparer.run(
463
+ { requestPayload, context: mergedContext },
464
+ { inspect },
465
+ ));
466
+ } else {
467
+ const result = await this.requestPreparer(requestPayload, mergedContext);
468
+ if (result) {
469
+ requestPayload = result;
470
+ }
471
+ }
472
+ }
473
+ inspect && logDeep({ requestPayload, mergedContext });
474
+ inspect && await askQuestion('?');
475
+
476
+ let response = await customFetch(
477
+ requestPayload.url,
478
+ requestPayload,
479
+ );
480
+ inspect && logDeep({ response });
481
+ inspect && await askQuestion('?');
482
+
483
+ const usedResponseInterpreter = responseInterpreter || this.responseInterpreter;
484
+ if (usedResponseInterpreter) {
485
+ if (usedResponseInterpreter.run) {
486
+ ({ response, context: mergedContext } = await usedResponseInterpreter.run(
487
+ { response, context: mergedContext },
488
+ { inspect },
489
+ ));
490
+ } else {
491
+ response = await usedResponseInterpreter(response, mergedContext);
492
+ }
493
+ }
494
+ inspect && logDeep({ response });
495
+ inspect && await askQuestion('?');
496
+
497
+ return response;
498
+ }
499
+ }
500
+
501
+ const fetchClientCommonSteps = {
502
+ stripEdgesAndNodes: async (state) => {
503
+ const { response } = state;
504
+
505
+ if (!response?.ok || !response?.data) {
506
+ return {};
507
+ }
508
+
509
+ return {
510
+ response: {
511
+ data: stripGraphqlEdgesAndNodes(response.data),
512
+ },
513
+ };
514
+ },
515
+ collapseDataWithOneValue: async (state) => {
516
+ const { response } = state;
517
+
518
+ if (!response?.ok || !response?.data) {
519
+ return {};
520
+ }
521
+
522
+ return {
523
+ response: {
524
+ data: collapseDataOnlyWrappers(response.data),
525
+ },
526
+ };
527
+ },
528
+ inspect: async (state) => {
529
+ logDeep({ state });
530
+ await askQuestion('?');
531
+ return {};
532
+ },
533
+ exitEarlyOnNotOk: async (state) => {
534
+ const { response } = state;
535
+
536
+ if (!response?.ok) {
537
+ return { breakChain: true };
538
+ }
539
+
540
+ return {};
541
+ },
542
+ exitEarlyOnGraphqlErrors: async (state) => {
543
+ const { response } = state;
544
+ const errors = response?.data?.errors;
545
+
546
+ if (!Array.isArray(errors) || !errors.length) {
547
+ return {};
548
+ }
549
+
550
+ const message = errors
551
+ .map((error) => error?.message)
552
+ .filter(Boolean)
553
+ .join('; ');
554
+
555
+ return {
556
+ response: {
557
+ ok: false,
558
+ error: {
559
+ code: 'GRAPHQL_ERROR',
560
+ message: message || 'GraphQL request failed',
561
+ details: errors,
562
+ },
563
+ },
564
+ breakChain: true,
565
+ };
566
+ },
567
+ digToPath: async (state) => {
568
+ const { response, context } = state;
569
+ const { resultPath } = context;
570
+ const { data } = response;
571
+
572
+ if (!data || !resultPath) {
573
+ return {};
574
+ }
575
+
576
+ const resultPathNodes = pathAsArray(resultPath);
577
+ const dataAtPath = objectDigNodeAtPath(data, resultPathNodes);
578
+
579
+ if (!dataAtPath) {
580
+ return {
581
+ response: {
582
+ ok: false,
583
+ error: {
584
+ code: 'DIG_FAILED',
585
+ message: `Data not found at path ${ resultPath }`,
586
+ },
587
+ },
588
+ breakChain: true,
589
+ };
590
+ }
591
+
592
+ return {
593
+ response: {
594
+ data: dataAtPath,
595
+ },
596
+ };
597
+ },
598
+ };
599
+
600
+ const valueProvided = (value) => value !== undefined && value !== null;
601
+
602
+ const ensureArray = (value) => {
603
+ if (Array.isArray(value)) {
604
+ return value;
605
+ }
606
+ return [value];
607
+ };
608
+
609
+ const everyIfArray = (func, value) => {
610
+ if (Array.isArray(value)) {
611
+ if (value.length === 0) {
612
+ return false;
613
+ }
614
+
615
+ // Validators should always return truthy values, right?
616
+ return value.map(func).every(i => i);
617
+ }
618
+ return func(value);
619
+ };
620
+
621
+ const timeMs = {
622
+ seconds: (number) => number * 1000,
623
+ minutes: (number) => number * 1000 * 60,
624
+ hours: (number) => number * 1000 * 60 * 60,
625
+ days: (number) => number * 1000 * 60 * 60 * 24,
626
+ weeks: (number) => number * 1000 * 60 * 60 * 24 * 7,
627
+ monthsish: (number) => number * 1000 * 60 * 60 * 24 * 30,
628
+ yearsish: (number) => number * 1000 * 60 * 60 * 24 * 365,
629
+ };
630
+
631
+ const responseArrayToResponse = (responses, { flatten = false } = {}) => {
632
+ if (responses.length === 1) {
633
+ return responses[0];
634
+ }
635
+
636
+ const succeeded = responses.filter(r => r?.ok).length;
637
+ const failed = responses.length - succeeded;
638
+
639
+ const response = {
640
+ ok: failed === 0,
641
+ results: responses,
642
+ meta: {
643
+ total: responses.length,
644
+ succeeded,
645
+ failed,
646
+ },
647
+ };
648
+
649
+ if (flatten) {
650
+ const data = responses
651
+ .map(r => r?.data)
652
+ .filter(d => d !== undefined);
653
+
654
+ if (data.length) {
655
+ response.data = data.flat();
656
+ }
657
+ }
658
+
659
+ return response;
660
+ };
661
+
662
+ class Operation {
663
+ constructor(func, { args = [], options = {} } = {}) {
664
+ this.func = func;
665
+ this.args = args;
666
+ this.options = options;
667
+ }
668
+
669
+ async run() {
670
+ return this.func(...this.args, this.options);
671
+ }
672
+ }
673
+
674
+ class OperationQueue {
675
+ constructor(operations) {
676
+
677
+ if (operations && operations.some(op => !(op instanceof Operation))) {
678
+ throw new Error('OperationQueue only takes Operations');
679
+ }
680
+
681
+ this.queue = operations || [];
682
+ }
683
+
684
+ add(operation) {
685
+ if (!(operation instanceof Operation)) {
686
+ throw new Error('OperationQueue only takes Operations');
687
+ }
688
+
689
+ this.queue.push(operation);
690
+ }
691
+
692
+ async run({
693
+ interval = false,
694
+ verbose = true,
695
+ } = {}) {
696
+
697
+ // Run at interval
698
+ if (interval) {
699
+ const results = new Array(this.queue.length);
700
+ let completedCount = 0;
701
+
702
+ // For each operation, run it without awaiting, and await the interval
703
+ for (const [i, op] of this.queue.entries()) {
704
+ (async () => {
705
+ const result = await op.run();
706
+ // Preserve result order
707
+ results[i] = result;
708
+ completedCount++;
709
+
710
+ if (verbose) {
711
+ console.log(`${ completedCount } / ${ this.queue.length }`);
712
+ }
713
+ })();
714
+
715
+ await wait(interval);
716
+ }
717
+
718
+ // Wait for all operations to complete
719
+ while (completedCount < this.queue.length) {
720
+ await wait(timeMs.seconds(1));
721
+ }
722
+
723
+ this.queue.length = 0;
724
+ return results;
725
+ }
726
+
727
+ // Run in sequence
728
+ const results = [];
729
+ for (const op of this.queue) {
730
+ const result = await op.run();
731
+ results.push(result);
732
+ if (verbose) {
733
+ console.log(`${ results.length } / ${ this.queue.length }`);
734
+ }
735
+ }
736
+
737
+ this.queue.length = 0;
738
+ return results;
739
+ }
740
+ }
741
+
742
+ const simpleSort = (arr, prop, { reverse } = {}) => {
743
+ return [...arr].sort((a, b) =>
744
+ reverse ? b[prop] - a[prop] : a[prop] - b[prop],
745
+ );
746
+ };
747
+
748
+ const arrayToChunks = (array, chunkSize, { chunkBy } = {}) => {
749
+ if (isNaN(chunkSize) || !(chunkSize > 0)) {
750
+ throw new Error(`arrayToChunks: chunkSize of ${ chunkSize } is invalid`);
751
+ }
752
+
753
+ if (!chunkBy) {
754
+ const chunks = [];
755
+ for (let i = 0; i < array.length; i += chunkSize) {
756
+ chunks.push(array.slice(i, i + chunkSize));
757
+ }
758
+ return chunks;
759
+ }
760
+
761
+ const sortedArray = simpleSort(array, chunkBy);
762
+ const chunks = [];
763
+ let currentChunk = [];
764
+ let currentGroupValue = null;
765
+
766
+ for (const item of sortedArray) {
767
+ const itemGroupValue = item[chunkBy];
768
+
769
+ const shouldStartNewChunk =
770
+ currentChunk.length >= chunkSize && itemGroupValue !== currentGroupValue;
771
+
772
+ if (shouldStartNewChunk && currentChunk.length > 0) {
773
+ chunks.push(currentChunk);
774
+ currentChunk = [];
775
+ }
776
+
777
+ currentChunk.push(item);
778
+ currentGroupValue = itemGroupValue;
779
+ }
780
+
781
+ if (currentChunk.length > 0) {
782
+ chunks.push(currentChunk);
783
+ }
784
+
785
+ return chunks;
786
+ };
787
+
788
+ const arraysToCartesianProduct = (arrayOfMixed) => {
789
+ return arrayOfMixed
790
+ .map(ensureArray)
791
+ .reduce((acc, curr) => {
792
+ return acc.flatMap(a => curr.map(b => [...a, b]));
793
+ }, [[]]);
794
+ };
795
+
796
+ const actionSingleOrMultiple = async (input, func, buildOpArgs, { queueRunOptions = {} } = {}) => {
797
+
798
+ /* How different inputs are handled:
799
+ 1. Single input
800
+ Pass directly to args builder function, and return result.
801
+ 2. Single array
802
+ Action one operation per value in the array.
803
+ 3. Array of arrays
804
+ Action all permutations of all values in the arrays.
805
+ */
806
+
807
+ // #1: Single input
808
+ // Pass directly to args builder function, and return result.
809
+ if (!Array.isArray(input)) {
810
+ return new Operation(func, buildOpArgs(input)).run();
811
+ }
812
+
813
+ let queue;
814
+
815
+ const argsBuilderArgsCount = buildOpArgs.length;
816
+ if (argsBuilderArgsCount <= 1) {
817
+ // #2: One input, as array
818
+ // Action one operation per value in the array.
819
+ queue = new OperationQueue(input.map(inputItem => new Operation(
820
+ func,
821
+ buildOpArgs(inputItem),
822
+ )));
823
+ } else {
824
+ // #3: Multiple inputs
825
+ // Action all combinations of all values in the arrays.
826
+ const combinations = arraysToCartesianProduct(input);
827
+ queue = new OperationQueue(combinations.map(combo => new Operation(
828
+ func,
829
+ buildOpArgs(...combo),
830
+ )));
831
+ }
832
+
833
+ const queueResponses = await queue.run(queueRunOptions);
834
+ return responseArrayToResponse(queueResponses);
835
+ };
836
+
837
+ const responseResultsByOutcome = (results) => {
838
+ const successes = [];
839
+ const failures = [];
840
+
841
+ for (const result of results) {
842
+ if (result?.ok) {
843
+ successes.push(result);
844
+ } else {
845
+ failures.push(result);
846
+ }
847
+ }
848
+
849
+ return { successes, failures };
850
+ };
851
+
852
+ const ifTextThenSpace = (text) => text ? `${ text } ` : '';
853
+
854
+ class Processor extends EventEmitter {
855
+ constructor(
856
+ pile,
857
+ action,
858
+ {
859
+ // defaults work for arrays
860
+ pileExhaustedCheck = () => pile?.length === 0,
861
+ pileSizeCheck = () => pile?.length,
862
+
863
+ logFlavourText,
864
+ maxInFlightRequests = 10, // Prevent interval-mode hung actions from piling up
865
+ runOptions = {},
866
+
867
+ canFinish = true,
868
+ onDone,
869
+ } = {},
870
+ ) {
871
+ super();
872
+
873
+ this.pile = pile;
874
+ this.action = action;
875
+ this.pileExhaustedCheck = pileExhaustedCheck;
876
+ this.canFinish = canFinish;
877
+ this.pileSizeCheck = pileSizeCheck;
878
+ this.logFlavourText = logFlavourText;
879
+ this.maxInFlightRequests = maxInFlightRequests;
880
+ this.runOptions = runOptions;
881
+
882
+ if (onDone) {
883
+ this.on('done', onDone);
884
+ }
885
+ }
886
+
887
+ getPileSize() {
888
+ return this.pileSizeCheck(this.pile);
889
+ }
890
+
891
+ getPileExhausted() {
892
+ return this.pileExhaustedCheck(this.pile);
893
+ }
894
+
895
+ async run(
896
+ options = {},
897
+ ) {
898
+
899
+ const {
900
+ interval = false,
901
+ verbose = true,
902
+ } = { ...this.runOptions, ...options };
903
+
904
+ let finished = false;
905
+ let results = [];
906
+
907
+ let startedCount = 0;
908
+ let completedCount = 0;
909
+
910
+ const initialSize = this.getPileSize();
911
+
912
+ const executeAction = async () => {
913
+ const actionResult = await this.action(this.pile);
914
+ results.push(actionResult);
915
+ completedCount++;
916
+
917
+ const pileSize = this.getPileSize();
918
+ verbose && console.log(`${ ifTextThenSpace(this.logFlavourText) }${ completedCount }/${ typeof initialSize === 'number' && initialSize > 0 ? `${ initialSize } > ` : '' }${ typeof pileSize === 'number' ? `${ pileSize }` : '?' }`);
919
+ };
920
+
921
+ while (!finished) {
922
+
923
+ const pileExhausted = this.getPileExhausted();
924
+
925
+ if (pileExhausted) {
926
+
927
+ // If interval, wait for all results to be in
928
+ if (this.canFinish && interval && (startedCount !== completedCount)) {
929
+ verbose && console.log(`${ ifTextThenSpace(this.logFlavourText) }waiting for all operations to complete`);
930
+ await wait(1000);
931
+ continue;
932
+ }
933
+
934
+ if (this.canFinish) {
935
+ finished = true;
936
+ break;
937
+ }
938
+
939
+ verbose && console.log(`${ ifTextThenSpace(this.logFlavourText) }waiting for permission to finish`);
940
+ await wait(3000);
941
+ continue;
942
+ }
943
+
944
+ if (this.maxInFlightRequests) {
945
+ const requestsInFlight = startedCount - completedCount;
946
+ // console.log('requestsInFlight', requestsInFlight);
947
+ if (requestsInFlight >= this.maxInFlightRequests) {
948
+ verbose && console.log(`${ ifTextThenSpace(this.logFlavourText) }hitting max in flight requests, waiting for some to complete`);
949
+ await wait(3000);
950
+ continue;
951
+ }
952
+ }
953
+
954
+ startedCount++;
955
+
956
+ if (interval) {
957
+ (async () => {
958
+ await executeAction();
959
+ })();
960
+
961
+ await wait(interval);
962
+ continue;
963
+ }
964
+
965
+ await executeAction();
966
+ }
967
+
968
+ verbose && console.log(`${ ifTextThenSpace(this.logFlavourText) }finished`);
969
+ this.emit('done');
970
+ return results;
971
+ }
972
+ }
973
+
974
+ class Getter extends EventEmitter {
975
+ constructor(
976
+ startingParams,
977
+ {
978
+ paginator,
979
+ digester,
980
+
981
+ func = customFetch,
982
+
983
+ limit,
984
+ // TODO: returnAllItems option or runAsFunction option
985
+
986
+ onItems,
987
+ onDone,
988
+ } = {},
989
+ ) {
990
+ super();
991
+
992
+ this.startingParams = {
993
+ args: startingParams?.args || [],
994
+ options: startingParams?.options || {},
995
+ };
996
+
997
+ this.paginator = paginator;
998
+ this.digester = digester;
999
+
1000
+ this.func = func;
1001
+
1002
+ this.limit = limit;
1003
+
1004
+ if (onItems) {
1005
+ this.on('items', onItems);
1006
+ }
1007
+
1008
+ if (onDone) {
1009
+ this.on('done', onDone);
1010
+ }
1011
+ }
1012
+
1013
+ async run({
1014
+ returnAll = false,
1015
+ } = {}) {
1016
+
1017
+ const { paginator, digester } = this;
1018
+
1019
+ let resultsCount = 0;
1020
+ let done = false;
1021
+ let paginatedParams = this.startingParams;
1022
+
1023
+ let allResults;
1024
+ if (returnAll) {
1025
+ allResults = [];
1026
+ }
1027
+
1028
+ while (!done) {
1029
+ const response = await this.func(
1030
+ ...paginatedParams.args,
1031
+ paginatedParams.options,
1032
+ );
1033
+
1034
+ logDeep(response);
1035
+
1036
+ let items;
1037
+ if (digester) {
1038
+ items = await digester(response);
1039
+ } else {
1040
+ items = ensureArray(response);
1041
+ }
1042
+
1043
+ if (this.limit) {
1044
+ const itemsLeft = this.limit - resultsCount;
1045
+ items = items.slice(0, itemsLeft);
1046
+ }
1047
+
1048
+ this.emit('items', items);
1049
+ resultsCount += items.length;
1050
+
1051
+ if (returnAll) {
1052
+ allResults.push(...items);
1053
+ }
1054
+
1055
+ if (this.limit && resultsCount >= this.limit) {
1056
+ done = true;
1057
+ break;
1058
+ }
1059
+
1060
+ [done, paginatedParams] = await paginator(paginatedParams, response);
1061
+ }
1062
+
1063
+ console.log('resultsCount', resultsCount);
1064
+ this.emit('done');
1065
+
1066
+ if (returnAll) {
1067
+ return allResults;
1068
+ }
1069
+ }
1070
+ }
1071
+
1072
+ class ArgsWarden {
1073
+ constructor(argValidatorTuples) {
1074
+ this.config = {};
1075
+ for (const [arg, validator] of argValidatorTuples) {
1076
+ this.config[arg] = validator || valueProvided;
1077
+ }
1078
+ }
1079
+
1080
+ argNames() {
1081
+ return Object.keys(this.config);
1082
+ }
1083
+
1084
+ async validate(
1085
+ args,
1086
+ context = {},
1087
+ ) {
1088
+
1089
+ for (const [argName, argValue] of Object.entries(args)) {
1090
+ const validator = this.config[argName];
1091
+ const valid = await validator(argValue, context);
1092
+
1093
+ if (!valid) {
1094
+ return false;
1095
+ }
1096
+ }
1097
+
1098
+ return true;
1099
+ }
1100
+
1101
+ async validateToMessages(
1102
+ args,
1103
+ context = {},
1104
+ ) {
1105
+ const rejectArgsMessages = [];
1106
+
1107
+ for (const [argName, argValue] of Object.entries(args)) {
1108
+ const validator = this.config[argName];
1109
+ const valid = await validator(argValue, context);
1110
+
1111
+ if (!valid) {
1112
+ rejectArgsMessages.push(`Invalid '${ argName }'`);
1113
+ }
1114
+ }
1115
+
1116
+ return rejectArgsMessages;
1117
+ }
1118
+
1119
+ async responseIfRejectingArgs(...args) {
1120
+ const rejectArgsMessages = await this.validateToMessages(...args);
1121
+
1122
+ if (rejectArgsMessages.length > 0) {
1123
+ return {
1124
+ ok: false,
1125
+ error: {
1126
+ code: 'INVALID_ARGS',
1127
+ message: rejectArgsMessages.join('; '),
1128
+ ...(rejectArgsMessages.length > 1 ? { details: rejectArgsMessages } : {}),
1129
+ },
1130
+ };
1131
+ }
1132
+
1133
+ return false;
1134
+ }
1135
+ }
1136
+
1137
+ module.exports = {
1138
+ wait,
1139
+ timeMs,
1140
+ objHasAny,
1141
+ capitaliseString,
1142
+ credsFromPayload,
1143
+ customFetch,
1144
+ logDeep,
1145
+ askQuestion,
1146
+ pathAsArray,
1147
+ objectDigNodeAtPath,
1148
+ appendUrlToBase,
1149
+ Chain,
1150
+ FetchClient,
1151
+ fetchClientCommonSteps,
1152
+ ensureArray,
1153
+ everyIfArray,
1154
+ simpleSort,
1155
+ arrayToChunks,
1156
+ responseArrayToResponse,
1157
+ responseResultsByOutcome,
1158
+ Operation,
1159
+ OperationQueue,
1160
+ actionSingleOrMultiple,
1161
+ Processor,
1162
+ Getter,
1163
+ capitaliseString,
1164
+ sentenceCaseString,
1165
+ ArgsWarden,
1166
+ };