zapier-platform-cli 15.16.1 → 15.17.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/README-source.md +31 -2
- package/README.md +35 -5
- package/oclif.manifest.json +1 -1
- package/package.json +4 -2
- package/src/generators/index.js +49 -11
- package/src/generators/templates/dynamic-dropdown/triggers/people.js +6 -1
- package/src/generators/templates/gitignore +1 -0
- package/src/generators/templates/typescript/README.md +2 -16
- package/src/generators/templates/typescript/index.js +1 -1
- package/src/generators/templates/typescript/src/creates/movie.ts +3 -8
- package/src/generators/templates/typescript/src/index.ts +5 -9
- package/src/generators/templates/typescript/src/test/creates.test.ts +1 -2
- package/src/generators/templates/typescript/src/test/triggers.test.ts +2 -3
- package/src/generators/templates/typescript/src/triggers/movie.ts +4 -3
- package/src/generators/templates/typescript/tsconfig.json +11 -6
- package/src/oclif/ZapierBaseCommand.js +1 -1
- package/src/oclif/commands/invoke.js +413 -45
- package/src/utils/convert.js +2 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
const
|
|
1
|
+
const crypto = require('node:crypto');
|
|
2
|
+
const fs = require('node:fs/promises');
|
|
3
|
+
const http = require('node:http');
|
|
2
4
|
|
|
3
5
|
const _ = require('lodash');
|
|
4
6
|
const { flags } = require('@oclif/command');
|
|
@@ -11,7 +13,7 @@ const { DateTime, IANAZone } = require('luxon');
|
|
|
11
13
|
|
|
12
14
|
const BaseCommand = require('../ZapierBaseCommand');
|
|
13
15
|
const { buildFlags } = require('../buildFlags');
|
|
14
|
-
const { localAppCommand } = require('../../utils/local');
|
|
16
|
+
const { localAppCommand, getLocalAppHandler } = require('../../utils/local');
|
|
15
17
|
const { startSpinner, endSpinner } = require('../../utils/display');
|
|
16
18
|
|
|
17
19
|
const ACTION_TYPE_PLURALS = {
|
|
@@ -224,6 +226,15 @@ const resolveInputDataTypes = (inputData, inputFields, timezone) => {
|
|
|
224
226
|
return inputData;
|
|
225
227
|
};
|
|
226
228
|
|
|
229
|
+
const appendEnv = async (vars, prefix = '') => {
|
|
230
|
+
await fs.appendFile(
|
|
231
|
+
'.env',
|
|
232
|
+
Object.entries(vars)
|
|
233
|
+
.filter(([k, v]) => v !== undefined)
|
|
234
|
+
.map(([k, v]) => `${prefix}${k}='${v || ''}'\n`)
|
|
235
|
+
);
|
|
236
|
+
};
|
|
237
|
+
|
|
227
238
|
const testAuth = async (authData, meta, zcacheTestObj) => {
|
|
228
239
|
startSpinner('Invoking authentication.test');
|
|
229
240
|
const result = await localAppCommand({
|
|
@@ -231,7 +242,10 @@ const testAuth = async (authData, meta, zcacheTestObj) => {
|
|
|
231
242
|
method: 'authentication.test',
|
|
232
243
|
bundle: {
|
|
233
244
|
authData,
|
|
234
|
-
meta
|
|
245
|
+
meta: {
|
|
246
|
+
...meta,
|
|
247
|
+
isTestingAuth: true,
|
|
248
|
+
},
|
|
235
249
|
},
|
|
236
250
|
zcacheTestObj,
|
|
237
251
|
customLogger,
|
|
@@ -300,7 +314,304 @@ const customLogger = (message, data) => {
|
|
|
300
314
|
debug(data);
|
|
301
315
|
};
|
|
302
316
|
|
|
317
|
+
const formatFieldDisplay = (field) => {
|
|
318
|
+
const ftype = field.type || 'string';
|
|
319
|
+
let result;
|
|
320
|
+
if (field.label) {
|
|
321
|
+
result = `${field.label} | ${field.key} | ${ftype}`;
|
|
322
|
+
} else {
|
|
323
|
+
result = `${field.key} | ${ftype}`;
|
|
324
|
+
}
|
|
325
|
+
if (field.required) {
|
|
326
|
+
result += ' | required';
|
|
327
|
+
}
|
|
328
|
+
return result;
|
|
329
|
+
};
|
|
330
|
+
|
|
303
331
|
class InvokeCommand extends BaseCommand {
|
|
332
|
+
async promptForAuthFields(authFields) {
|
|
333
|
+
const authData = {};
|
|
334
|
+
for (const field of authFields) {
|
|
335
|
+
if (field.computed) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const message = formatFieldDisplay(field) + ':';
|
|
339
|
+
let value;
|
|
340
|
+
if (field.type === 'password') {
|
|
341
|
+
value = await this.promptHidden(message, true);
|
|
342
|
+
} else {
|
|
343
|
+
value = await this.prompt(message, { useStderr: true });
|
|
344
|
+
}
|
|
345
|
+
authData[field.key] = value;
|
|
346
|
+
}
|
|
347
|
+
return authData;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async startBasicAuth(authFields) {
|
|
351
|
+
if (this.nonInteractive) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
'The `auth start` subcommand for "basic" authentication type only works in interactive mode.'
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
return this.promptForAuthFields([
|
|
357
|
+
{
|
|
358
|
+
key: 'username',
|
|
359
|
+
label: 'Username',
|
|
360
|
+
required: true,
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
key: 'password',
|
|
364
|
+
label: 'Password',
|
|
365
|
+
required: true,
|
|
366
|
+
},
|
|
367
|
+
]);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async startCustomAuth(authFields) {
|
|
371
|
+
if (this.nonInteractive) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
'The `auth start` subcommand for "custom" authentication type only works in interactive mode.'
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
return this.promptForAuthFields(authFields);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async startOAuth2(appDefinition) {
|
|
380
|
+
const redirectUri = this.flags['redirect-uri'];
|
|
381
|
+
const port = this.flags['local-port'];
|
|
382
|
+
const env = {};
|
|
383
|
+
|
|
384
|
+
if (!process.env.CLIENT_ID || !process.env.CLIENT_SECRET) {
|
|
385
|
+
if (this.nonInteractive) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
'CLIENT_ID and CLIENT_SECRET must be set in the .env file in non-interactive mode.'
|
|
388
|
+
);
|
|
389
|
+
} else {
|
|
390
|
+
console.warn(
|
|
391
|
+
'CLIENT_ID and CLIENT_SECRET are required for OAuth2, ' +
|
|
392
|
+
"but they are not found in the .env file. I'll prompt you for them now."
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!process.env.CLIENT_ID) {
|
|
398
|
+
env.CLIENT_ID = await this.prompt('CLIENT_ID:', { useStderr: true });
|
|
399
|
+
process.env.CLIENT_ID = env.CLIENT_ID;
|
|
400
|
+
}
|
|
401
|
+
if (!process.env.CLIENT_SECRET) {
|
|
402
|
+
env.CLIENT_SECRET = await this.prompt('CLIENT_SECRET:', {
|
|
403
|
+
useStderr: true,
|
|
404
|
+
});
|
|
405
|
+
process.env.CLIENT_SECRET = env.CLIENT_SECRET;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (!_.isEmpty(env)) {
|
|
409
|
+
// process.env changed, so we need to reload the modules that have loaded
|
|
410
|
+
// the old values of process.env
|
|
411
|
+
getLocalAppHandler({ reload: true });
|
|
412
|
+
|
|
413
|
+
// Save envs so the user won't have to re-enter them if the command fails
|
|
414
|
+
await appendEnv(env);
|
|
415
|
+
console.warn('CLIENT_ID and CLIENT_SECRET saved to .env file.');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
startSpinner('Invoking authentication.oauth2Config.authorizeUrl');
|
|
419
|
+
|
|
420
|
+
const stateParam = crypto.randomBytes(20).toString('hex');
|
|
421
|
+
let authorizeUrl = await localAppCommand({
|
|
422
|
+
command: 'execute',
|
|
423
|
+
method: 'authentication.oauth2Config.authorizeUrl',
|
|
424
|
+
bundle: {
|
|
425
|
+
inputData: {
|
|
426
|
+
response_type: 'code',
|
|
427
|
+
redirect_uri: redirectUri,
|
|
428
|
+
state: stateParam,
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
if (!authorizeUrl.includes('&scope=')) {
|
|
433
|
+
const scope = appDefinition.authentication.oauth2Config.scope;
|
|
434
|
+
if (scope) {
|
|
435
|
+
authorizeUrl += `&scope=${encodeURIComponent(scope)}`;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
debug('authorizeUrl:', authorizeUrl);
|
|
439
|
+
|
|
440
|
+
endSpinner();
|
|
441
|
+
startSpinner('Starting local HTTP server');
|
|
442
|
+
|
|
443
|
+
let resolveCode;
|
|
444
|
+
const codePromise = new Promise((resolve) => {
|
|
445
|
+
resolveCode = resolve;
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const server = http.createServer((req, res) => {
|
|
449
|
+
// Parse the request URL to extract the query parameters
|
|
450
|
+
const code = new URL(req.url, redirectUri).searchParams.get('code');
|
|
451
|
+
if (code) {
|
|
452
|
+
resolveCode(code);
|
|
453
|
+
debug(`Received code '${code}' from ${req.headers.referer}`);
|
|
454
|
+
|
|
455
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
456
|
+
res.end(
|
|
457
|
+
'Parameter `code` received successfully. Go back to the terminal to continue.'
|
|
458
|
+
);
|
|
459
|
+
} else {
|
|
460
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
461
|
+
res.end(
|
|
462
|
+
'Error: Did not receive `code` query parameter. ' +
|
|
463
|
+
'Did you have the right CLIENT_ID and CLIENT_SECRET? ' +
|
|
464
|
+
'Or did your server respond properly?'
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
await new Promise((resolve) => {
|
|
470
|
+
server.listen(port, resolve);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
endSpinner();
|
|
474
|
+
startSpinner(
|
|
475
|
+
'Opening browser to authorize (press Ctrl-C to exit on error)'
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
const { default: open } = await import('open');
|
|
479
|
+
open(authorizeUrl);
|
|
480
|
+
|
|
481
|
+
const code = await codePromise;
|
|
482
|
+
endSpinner();
|
|
483
|
+
|
|
484
|
+
startSpinner('Closing local HTTP server');
|
|
485
|
+
await new Promise((resolve) => {
|
|
486
|
+
server.close(resolve);
|
|
487
|
+
});
|
|
488
|
+
debug('Local HTTP server closed');
|
|
489
|
+
|
|
490
|
+
endSpinner();
|
|
491
|
+
startSpinner('Invoking authentication.oauth2Config.getAccessToken');
|
|
492
|
+
|
|
493
|
+
const authData = await localAppCommand({
|
|
494
|
+
command: 'execute',
|
|
495
|
+
method: 'authentication.oauth2Config.getAccessToken',
|
|
496
|
+
bundle: {
|
|
497
|
+
inputData: {
|
|
498
|
+
code,
|
|
499
|
+
redirect_uri: redirectUri,
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
endSpinner();
|
|
505
|
+
return authData;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async startSessionAuth(appDefinition) {
|
|
509
|
+
if (this.nonInteractive) {
|
|
510
|
+
throw new Error(
|
|
511
|
+
'The `auth start` subcommand for "session" authentication type only works in interactive mode.'
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
const authData = await this.promptForAuthFields(
|
|
515
|
+
appDefinition.authentication.fields
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
startSpinner('Invoking authentication.sessionConfig.perform');
|
|
519
|
+
const sessionData = await localAppCommand({
|
|
520
|
+
command: 'execute',
|
|
521
|
+
method: 'authentication.sessionConfig.perform',
|
|
522
|
+
bundle: {
|
|
523
|
+
authData,
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
endSpinner();
|
|
527
|
+
|
|
528
|
+
return { ...authData, ...sessionData };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async startAuth(appDefinition) {
|
|
532
|
+
const authentication = appDefinition.authentication;
|
|
533
|
+
if (!authentication) {
|
|
534
|
+
console.warn(
|
|
535
|
+
"Your integration doesn't seem to need authentication. " +
|
|
536
|
+
"If that isn't true, the app definition should have " +
|
|
537
|
+
'an `authentication` object at the root level.'
|
|
538
|
+
);
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
switch (authentication.type) {
|
|
542
|
+
case 'basic':
|
|
543
|
+
return this.startBasicAuth(authentication.fields);
|
|
544
|
+
case 'custom':
|
|
545
|
+
return this.startCustomAuth(authentication.fields);
|
|
546
|
+
case 'oauth2':
|
|
547
|
+
return this.startOAuth2(appDefinition);
|
|
548
|
+
case 'session':
|
|
549
|
+
return this.startSessionAuth(appDefinition);
|
|
550
|
+
default:
|
|
551
|
+
// TODO: Add support for 'digest' and 'oauth1'
|
|
552
|
+
throw new Error(
|
|
553
|
+
`This command doesn't support authentication type "${authentication.type}".`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async refreshOAuth2(appDefinition, authData) {
|
|
559
|
+
startSpinner('Invoking authentication.oauth2Config.refreshAccessToken');
|
|
560
|
+
|
|
561
|
+
const newAuthData = await localAppCommand({
|
|
562
|
+
command: 'execute',
|
|
563
|
+
method: 'authentication.oauth2Config.refreshAccessToken',
|
|
564
|
+
bundle: {
|
|
565
|
+
authData,
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
endSpinner();
|
|
570
|
+
return newAuthData;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async refreshSessionAuth(appDefinition, authData) {
|
|
574
|
+
startSpinner('Invoking authentication.sessionConfig.perform');
|
|
575
|
+
|
|
576
|
+
const sessionData = await localAppCommand({
|
|
577
|
+
command: 'execute',
|
|
578
|
+
method: 'authentication.sessionConfig.perform',
|
|
579
|
+
bundle: {
|
|
580
|
+
authData,
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
endSpinner();
|
|
585
|
+
return sessionData;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async refreshAuth(appDefinition, authData) {
|
|
589
|
+
const authentication = appDefinition.authentication;
|
|
590
|
+
if (!authentication) {
|
|
591
|
+
console.warn(
|
|
592
|
+
"Your integration doesn't seem to need authentication. " +
|
|
593
|
+
"If that isn't true, the app definition should have " +
|
|
594
|
+
'an `authentication` object at the root level.'
|
|
595
|
+
);
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
if (_.isEmpty(authData)) {
|
|
599
|
+
throw new Error(
|
|
600
|
+
'No auth data found in the .env file. Run `zapier invoke auth start` first to initialize the auth data.'
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
switch (authentication.type) {
|
|
604
|
+
case 'oauth2':
|
|
605
|
+
return this.refreshOAuth2(appDefinition, authData);
|
|
606
|
+
case 'session':
|
|
607
|
+
return this.refreshSessionAuth(appDefinition, authData);
|
|
608
|
+
default:
|
|
609
|
+
throw new Error(
|
|
610
|
+
`This command doesn't support refreshing authentication type "${authentication.type}".`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
304
615
|
async promptForField(
|
|
305
616
|
field,
|
|
306
617
|
appDefinition,
|
|
@@ -310,19 +621,7 @@ class InvokeCommand extends BaseCommand {
|
|
|
310
621
|
zcacheTestObj,
|
|
311
622
|
cursorTestObj
|
|
312
623
|
) {
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
let message;
|
|
316
|
-
if (field.label) {
|
|
317
|
-
message = `${field.label} | ${field.key} | ${ftype}`;
|
|
318
|
-
} else {
|
|
319
|
-
message = `${field.key} | ${ftype}`;
|
|
320
|
-
}
|
|
321
|
-
if (field.required) {
|
|
322
|
-
message += ' | required';
|
|
323
|
-
}
|
|
324
|
-
message += ':';
|
|
325
|
-
|
|
624
|
+
const message = formatFieldDisplay(field) + ':';
|
|
326
625
|
if (field.dynamic) {
|
|
327
626
|
// Dyanmic dropdown
|
|
328
627
|
const [triggerKey, idField, labelField] = field.dynamic.split('.');
|
|
@@ -358,7 +657,7 @@ class InvokeCommand extends BaseCommand {
|
|
|
358
657
|
}),
|
|
359
658
|
{ useStderr: true }
|
|
360
659
|
);
|
|
361
|
-
} else if (
|
|
660
|
+
} else if (field.type === 'boolean') {
|
|
362
661
|
const yes = await this.confirm(message, false, !field.required, true);
|
|
363
662
|
return yes ? 'yes' : 'no';
|
|
364
663
|
} else {
|
|
@@ -378,10 +677,10 @@ class InvokeCommand extends BaseCommand {
|
|
|
378
677
|
) {
|
|
379
678
|
const missingFields = getMissingRequiredInputFields(inputData, inputFields);
|
|
380
679
|
if (missingFields.length) {
|
|
381
|
-
if (
|
|
680
|
+
if (this.nonInteractive || meta.isFillingDynamicDropdown) {
|
|
382
681
|
throw new Error(
|
|
383
|
-
'
|
|
384
|
-
missingFields.map((f) =>
|
|
682
|
+
"You're in non-interactive mode, so you must at least specify these required fields with --inputData: \n" +
|
|
683
|
+
missingFields.map((f) => '* ' + formatFieldDisplay(f)).join('\n')
|
|
385
684
|
);
|
|
386
685
|
}
|
|
387
686
|
for (const f of missingFields) {
|
|
@@ -481,7 +780,7 @@ class InvokeCommand extends BaseCommand {
|
|
|
481
780
|
zcacheTestObj,
|
|
482
781
|
cursorTestObj
|
|
483
782
|
);
|
|
484
|
-
if (
|
|
783
|
+
if (!this.nonInteractive && !meta.isFillingDynamicDropdown) {
|
|
485
784
|
await this.promptForInputFieldEdit(
|
|
486
785
|
inputData,
|
|
487
786
|
inputFields,
|
|
@@ -581,14 +880,22 @@ class InvokeCommand extends BaseCommand {
|
|
|
581
880
|
}
|
|
582
881
|
|
|
583
882
|
async perform() {
|
|
584
|
-
dotenv.config({ override: true });
|
|
883
|
+
const dotenvResult = dotenv.config({ override: true });
|
|
884
|
+
if (_.isEmpty(dotenvResult.parsed)) {
|
|
885
|
+
console.warn(
|
|
886
|
+
'The .env file does not exist or is empty. ' +
|
|
887
|
+
'You may need to set some environment variables in there if your code uses process.env.'
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
this.nonInteractive = this.flags['non-interactive'] || !process.stdin.isTTY;
|
|
585
892
|
|
|
586
893
|
let { actionType, actionKey } = this.args;
|
|
587
894
|
|
|
588
895
|
if (!actionType) {
|
|
589
|
-
if (
|
|
896
|
+
if (this.nonInteractive) {
|
|
590
897
|
throw new Error(
|
|
591
|
-
'You must specify ACTIONTYPE and ACTIONKEY
|
|
898
|
+
'You must specify ACTIONTYPE and ACTIONKEY in non-interactive mode.'
|
|
592
899
|
);
|
|
593
900
|
}
|
|
594
901
|
actionType = await this.promptWithList(
|
|
@@ -602,11 +909,11 @@ class InvokeCommand extends BaseCommand {
|
|
|
602
909
|
const appDefinition = await localAppCommand({ command: 'definition' });
|
|
603
910
|
|
|
604
911
|
if (!actionKey) {
|
|
605
|
-
if (
|
|
606
|
-
throw new Error('You must specify ACTIONKEY
|
|
912
|
+
if (this.nonInteractive) {
|
|
913
|
+
throw new Error('You must specify ACTIONKEY in non-interactive mode.');
|
|
607
914
|
}
|
|
608
915
|
if (actionType === 'auth') {
|
|
609
|
-
const actionKeys = ['label', 'test'];
|
|
916
|
+
const actionKeys = ['label', 'refresh', 'start', 'test'];
|
|
610
917
|
actionKey = await this.promptWithList(
|
|
611
918
|
'Which auth operation would you like to invoke?',
|
|
612
919
|
actionKeys,
|
|
@@ -644,7 +951,28 @@ class InvokeCommand extends BaseCommand {
|
|
|
644
951
|
isTestingAuth: true,
|
|
645
952
|
};
|
|
646
953
|
switch (actionKey) {
|
|
647
|
-
|
|
954
|
+
case 'start': {
|
|
955
|
+
const newAuthData = await this.startAuth(appDefinition);
|
|
956
|
+
if (_.isEmpty(newAuthData)) {
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
await appendEnv(newAuthData, AUTH_FIELD_ENV_PREFIX);
|
|
960
|
+
console.warn(
|
|
961
|
+
'Auth data appended to .env file. Run `zapier invoke auth test` to test it.'
|
|
962
|
+
);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
case 'refresh': {
|
|
966
|
+
const newAuthData = await this.refreshAuth(appDefinition, authData);
|
|
967
|
+
if (_.isEmpty(newAuthData)) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
await appendEnv(newAuthData, AUTH_FIELD_ENV_PREFIX);
|
|
971
|
+
console.warn(
|
|
972
|
+
'Auth data has been refreshed and appended to .env file. Run `zapier invoke auth test` to test it.'
|
|
973
|
+
);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
648
976
|
case 'test': {
|
|
649
977
|
const output = await testAuth(authData, meta, zcacheTestObj);
|
|
650
978
|
console.log(JSON.stringify(output, null, 2));
|
|
@@ -654,7 +982,7 @@ class InvokeCommand extends BaseCommand {
|
|
|
654
982
|
const labelTemplate = appDefinition.authentication.connectionLabel;
|
|
655
983
|
if (labelTemplate && labelTemplate.startsWith('$func$')) {
|
|
656
984
|
console.warn(
|
|
657
|
-
'Function-based connection label is
|
|
985
|
+
'Function-based connection label is not supported yet. Printing auth test result instead.'
|
|
658
986
|
);
|
|
659
987
|
const output = await testAuth(authData, meta, zcacheTestObj);
|
|
660
988
|
console.log(JSON.stringify(output, null, 2));
|
|
@@ -674,7 +1002,10 @@ class InvokeCommand extends BaseCommand {
|
|
|
674
1002
|
return;
|
|
675
1003
|
}
|
|
676
1004
|
default:
|
|
677
|
-
throw new Error(
|
|
1005
|
+
throw new Error(
|
|
1006
|
+
`Unknown auth operation "${actionKey}". ` +
|
|
1007
|
+
'The options are "label", "refresh", "start", and "test". \n'
|
|
1008
|
+
);
|
|
678
1009
|
}
|
|
679
1010
|
} else {
|
|
680
1011
|
const action = appDefinition[actionTypePlural][actionKey];
|
|
@@ -694,7 +1025,8 @@ class InvokeCommand extends BaseCommand {
|
|
|
694
1025
|
if (filePath === '-') {
|
|
695
1026
|
inputStream = process.stdin;
|
|
696
1027
|
} else {
|
|
697
|
-
|
|
1028
|
+
const fd = await fs.open(filePath);
|
|
1029
|
+
inputStream = fd.createReadStream({ encoding: 'utf8' });
|
|
698
1030
|
}
|
|
699
1031
|
inputData = await readStream(inputStream);
|
|
700
1032
|
}
|
|
@@ -746,14 +1078,14 @@ InvokeCommand.flags = buildFlags({
|
|
|
746
1078
|
description:
|
|
747
1079
|
'The input data to pass to the action. Must be a JSON-encoded object. The data can be passed from the command directly like \'{"key": "value"}\', read from a file like @file.json, or read from stdin like @-.',
|
|
748
1080
|
}),
|
|
749
|
-
|
|
1081
|
+
isFillingDynamicDropdown: flags.boolean({
|
|
750
1082
|
description:
|
|
751
|
-
'Set bundle.meta.
|
|
1083
|
+
'Set bundle.meta.isFillingDynamicDropdown to true. Only makes sense for a polling trigger. When true in production, this poll is being used to populate a dynamic dropdown.',
|
|
752
1084
|
default: false,
|
|
753
1085
|
}),
|
|
754
|
-
|
|
1086
|
+
isLoadingSample: flags.boolean({
|
|
755
1087
|
description:
|
|
756
|
-
'Set bundle.meta.
|
|
1088
|
+
'Set bundle.meta.isLoadingSample to true. When true in production, this run is initiated by the user in the Zap editor trying to pull a sample.',
|
|
757
1089
|
default: false,
|
|
758
1090
|
}),
|
|
759
1091
|
isPopulatingDedupe: flags.boolean({
|
|
@@ -772,12 +1104,26 @@ InvokeCommand.flags = buildFlags({
|
|
|
772
1104
|
'Set bundle.meta.page. Only makes sense for a trigger. When used in production, this indicates which page of items you should fetch. First page is 0.',
|
|
773
1105
|
default: 0,
|
|
774
1106
|
}),
|
|
1107
|
+
'non-interactive': flags.boolean({
|
|
1108
|
+
description: 'Do not show interactive prompts.',
|
|
1109
|
+
default: false,
|
|
1110
|
+
}),
|
|
775
1111
|
timezone: flags.string({
|
|
776
1112
|
char: 'z',
|
|
777
1113
|
description:
|
|
778
|
-
'Set the default timezone for datetime
|
|
1114
|
+
'Set the default timezone for datetime field interpretation. If not set, defaults to America/Chicago, which matches Zapier production behavior. Find the list timezone names at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.',
|
|
779
1115
|
default: 'America/Chicago',
|
|
780
1116
|
}),
|
|
1117
|
+
'redirect-uri': flags.string({
|
|
1118
|
+
description:
|
|
1119
|
+
"Only used by `auth start` subcommand. The redirect URI that will be passed to the OAuth2 authorization URL. Usually this should match the one configured in your server's OAuth2 application settings. A local HTTP server will be started to listen for the OAuth2 callback. If your server requires a non-localhost or HTTPS address for the redirect URI, you can set up port forwarding to route the non-localhost or HTTPS address to localhost.",
|
|
1120
|
+
default: 'http://localhost:9000',
|
|
1121
|
+
}),
|
|
1122
|
+
'local-port': flags.integer({
|
|
1123
|
+
description:
|
|
1124
|
+
'Only used by `auth start` subcommand. The local port that will be used to start the local HTTP server to listen for the OAuth2 callback. This port can be different from the one in the redirect URI if you have port forwarding set up.',
|
|
1125
|
+
default: 9000,
|
|
1126
|
+
}),
|
|
781
1127
|
},
|
|
782
1128
|
});
|
|
783
1129
|
|
|
@@ -790,14 +1136,16 @@ InvokeCommand.args = [
|
|
|
790
1136
|
{
|
|
791
1137
|
name: 'actionKey',
|
|
792
1138
|
description:
|
|
793
|
-
'The trigger/action key you want to invoke. If ACTIONTYPE is "auth", this can be "
|
|
1139
|
+
'The trigger/action key you want to invoke. If ACTIONTYPE is "auth", this can be "label", "refresh", "start", or "test".',
|
|
794
1140
|
},
|
|
795
1141
|
];
|
|
796
1142
|
|
|
797
|
-
InvokeCommand.skipValidInstallCheck = true;
|
|
798
1143
|
InvokeCommand.examples = [
|
|
799
1144
|
'zapier invoke',
|
|
1145
|
+
'zapier invoke auth start',
|
|
1146
|
+
'zapier invoke auth refresh',
|
|
800
1147
|
'zapier invoke auth test',
|
|
1148
|
+
'zapier invoke auth label',
|
|
801
1149
|
'zapier invoke trigger new_recipe',
|
|
802
1150
|
`zapier invoke create add_recipe --inputData '{"title": "Pancakes"}'`,
|
|
803
1151
|
'zapier invoke search find_recipe -i @file.json',
|
|
@@ -807,11 +1155,23 @@ InvokeCommand.description = `Invoke an auth operation, a trigger, or a create/se
|
|
|
807
1155
|
|
|
808
1156
|
This command emulates how Zapier production environment would invoke your integration. It runs code locally, so you can use this command to quickly test your integration without deploying it to Zapier. This is especially useful for debugging and development.
|
|
809
1157
|
|
|
810
|
-
This command loads \`authData\` from the \`.env\` file in the current directory.
|
|
1158
|
+
This command loads environment variables and \`authData\` from the \`.env\` file in the current directory. If you don't have a \`.env\` file yet, you can use the \`zapier invoke auth start\` command to help you initialize it, or you can manually create it.
|
|
1159
|
+
|
|
1160
|
+
The \`zapier invoke auth start\` subcommand will prompt you for the necessary auth fields and save them to the \`.env\` file. For OAuth2, it will start a local HTTP server, open the authorization URL in the browser, wait for the OAuth2 redirect, and get the access token.
|
|
1161
|
+
|
|
1162
|
+
Each line in the \`.env\` file should follow one of these formats:
|
|
1163
|
+
|
|
1164
|
+
* \`VAR_NAME=VALUE\` for environment variables
|
|
1165
|
+
* \`authData_FIELD_KEY=VALUE\` for auth data fields
|
|
1166
|
+
|
|
1167
|
+
For example, a \`.env\` file for an OAuth2 integration might look like this:
|
|
811
1168
|
|
|
812
1169
|
\`\`\`
|
|
813
|
-
|
|
814
|
-
|
|
1170
|
+
CLIENT_ID='your_client_id'
|
|
1171
|
+
CLIENT_SECRET='your_client_secret'
|
|
1172
|
+
authData_access_token='1234567890'
|
|
1173
|
+
authData_refresh_token='abcdefg'
|
|
1174
|
+
authData_account_name='zapier'
|
|
815
1175
|
\`\`\`
|
|
816
1176
|
|
|
817
1177
|
To test if the auth data is correct, run either one of these:
|
|
@@ -821,7 +1181,9 @@ zapier invoke auth test # invokes authentication.test method
|
|
|
821
1181
|
zapier invoke auth label # invokes authentication.test and renders connection label
|
|
822
1182
|
\`\`\`
|
|
823
1183
|
|
|
824
|
-
|
|
1184
|
+
To refresh stale auth data for OAuth2 or session auth, run \`zapier invoke auth refresh\`.
|
|
1185
|
+
|
|
1186
|
+
Once you have the correct auth data, you can test an trigger, a search, or a create action. For example, here's how you invoke a trigger with the key \`new_recipe\`:
|
|
825
1187
|
|
|
826
1188
|
\`\`\`
|
|
827
1189
|
zapier invoke trigger new_recipe
|
|
@@ -829,10 +1191,12 @@ zapier invoke trigger new_recipe
|
|
|
829
1191
|
|
|
830
1192
|
To add input data, use the \`--inputData\` flag. The input data can come from the command directly, a file, or stdin. See **EXAMPLES** below.
|
|
831
1193
|
|
|
832
|
-
|
|
1194
|
+
When you miss any command arguments, such as ACTIONTYPE or ACTIONKEY, the command will prompt you interactively. If you don't want to get interactive prompts, use the \`--non-interactive\` flag.
|
|
1195
|
+
|
|
1196
|
+
The \`--debug\` flag will show you the HTTP request logs and any console logs you have in your code.
|
|
1197
|
+
|
|
1198
|
+
The following is a non-exhaustive list of current limitations and may be supported in the future:
|
|
833
1199
|
|
|
834
|
-
- \`zapier invoke auth start\` to help you initialize the auth data in \`.env\`
|
|
835
|
-
- \`zapier invoke auth refresh\` to refresh the auth data in \`.env\`
|
|
836
1200
|
- Hook triggers, including REST hook subscribe/unsubscribe
|
|
837
1201
|
- Line items
|
|
838
1202
|
- Output hydration
|
|
@@ -840,6 +1204,10 @@ The following are current limitations and may be supported in the future:
|
|
|
840
1204
|
- Dynamic dropdown pagination
|
|
841
1205
|
- Function-based connection label
|
|
842
1206
|
- Buffered create actions
|
|
1207
|
+
- Search-or-create actions
|
|
1208
|
+
- Search-powered fields
|
|
1209
|
+
- Field choices
|
|
1210
|
+
- autoRefresh for OAuth2 and session auth
|
|
843
1211
|
`;
|
|
844
1212
|
|
|
845
1213
|
module.exports = InvokeCommand;
|
package/src/utils/convert.js
CHANGED
|
@@ -251,6 +251,7 @@ const renderIndex = async (appDefinition) => {
|
|
|
251
251
|
triggers: 'Trigger',
|
|
252
252
|
creates: 'Create',
|
|
253
253
|
searches: 'Search',
|
|
254
|
+
bulkReads: 'BulkRead',
|
|
254
255
|
},
|
|
255
256
|
(importNameSuffix, stepType) => {
|
|
256
257
|
_.each(appDefinition[stepType], (definition, key) => {
|
|
@@ -380,7 +381,7 @@ const convertApp = async (appInfo, appDefinition, newAppDir) => {
|
|
|
380
381
|
|
|
381
382
|
const promises = [];
|
|
382
383
|
|
|
383
|
-
['triggers', 'creates', 'searches'].forEach((stepType) => {
|
|
384
|
+
['triggers', 'creates', 'searches', 'bulkReads'].forEach((stepType) => {
|
|
384
385
|
_.each(appDefinition[stepType], (definition, key) => {
|
|
385
386
|
promises.push(
|
|
386
387
|
writeStep(stepType, definition, key, newAppDir),
|