Milestone 0: import zappier billing, Verae middleware, and Zapier research
Compose-ready workspace: packages/zappier (rate card, portal, Stripe), packages/verae-zapier-middleware (timestamp + NATS), packages/verae-zapier (CLI app), vendor/zapier-platform, and research/zapier vendor corpus. Gate 0 structure checks pass. Product code and research are not yet wired.
This commit is contained in:
commit
b4150c8250
1364 changed files with 6814366 additions and 0 deletions
352
vendor/zapier-platform/packages/cli/src/oclif/ZapierBaseCommand.js
vendored
Normal file
352
vendor/zapier-platform/packages/cli/src/oclif/ZapierBaseCommand.js
vendored
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
const { Command } = require('@oclif/core');
|
||||
const colors = require('colors/safe');
|
||||
|
||||
const { startSpinner, endSpinner, formatStyles } = require('../utils/display');
|
||||
const { isValidAppInstall } = require('../utils/misc');
|
||||
const { recordAnalytics } = require('../utils/analytics');
|
||||
|
||||
const { getWritableApp } = require('../utils/api');
|
||||
|
||||
const inquirer = require('inquirer');
|
||||
const { throwForInvalidVersion } = require('../utils/version');
|
||||
|
||||
const DATA_FORMATS = ['json', 'raw'];
|
||||
|
||||
class ZapierBaseCommand extends Command {
|
||||
async run() {
|
||||
this._initPromptModules();
|
||||
await this._parseCommand();
|
||||
|
||||
if (this.flags.debug) {
|
||||
this.debug.enabled = true; // enables this.debug on the command
|
||||
require('debug').enable('zapier:*,oclif:zapier:*'); // enables all further spawned functions, like API
|
||||
}
|
||||
|
||||
this.debug('argv is', this.argv);
|
||||
this.debug('args are', this.args);
|
||||
this.debug('flags are', this.flags);
|
||||
this.debug('------------');
|
||||
|
||||
this.throwForInvalidAppInstall();
|
||||
|
||||
// the following comments are pre-merge, might be out of date:
|
||||
|
||||
// If the `perform` errors out, then we never see the analytics response. We also run the risk of not having the chance to fire them off at all
|
||||
// would need to catch errors in the perform so that they're not thrown until the whole chain finishes
|
||||
// also, would be nice to plug into something a little more base-level so we catch invalid flags. Not super important
|
||||
|
||||
return Promise.all([
|
||||
this._recordAnalytics(),
|
||||
|
||||
this.perform().catch((e) => {
|
||||
this.stopSpinner({ success: false });
|
||||
const errTextLines = [e.message];
|
||||
|
||||
this.debug(e.stack);
|
||||
|
||||
if (!this.flags.debug && !this.flags.invokedFromAnotherCommand) {
|
||||
errTextLines.push(
|
||||
colors.gray('re-run this command with `--debug` for more info'),
|
||||
);
|
||||
}
|
||||
|
||||
this.error(errTextLines.join('\n\n'));
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
get _staticClassReference() {
|
||||
return Object.getPrototypeOf(this).constructor;
|
||||
}
|
||||
|
||||
_initPromptModules() {
|
||||
this._stdoutPrompt = inquirer.prompt;
|
||||
this._stderrPrompt = inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
});
|
||||
}
|
||||
|
||||
async _parseCommand() {
|
||||
const { flags, args, argv } = await this.parse(this._staticClassReference);
|
||||
|
||||
this.flags = flags;
|
||||
this.args = args;
|
||||
this.argv = argv;
|
||||
}
|
||||
|
||||
perform() {
|
||||
this.error(`subclass the "perform" method in the "${this.id}" command`);
|
||||
}
|
||||
|
||||
// put ina method so we can disable it easily in tests
|
||||
throwForInvalidAppInstall() {
|
||||
if (this._staticClassReference.skipValidInstallCheck) {
|
||||
return;
|
||||
}
|
||||
const { valid, reason } = isValidAppInstall();
|
||||
if (!valid) {
|
||||
this.error(reason);
|
||||
}
|
||||
}
|
||||
|
||||
// validate that user input looks like a semver version
|
||||
throwForInvalidVersion(version) {
|
||||
return throwForInvalidVersion(version);
|
||||
}
|
||||
|
||||
async getWritableApp() {
|
||||
this.startSpinner('Checking authentication & permissions');
|
||||
const app = await getWritableApp();
|
||||
this.stopSpinner();
|
||||
return app;
|
||||
}
|
||||
|
||||
// UTILS
|
||||
/**
|
||||
* Helps us not have helpful UI messages when the whole output should only be JSON.
|
||||
* @param {...any} message the joined string to print out
|
||||
*/
|
||||
log(...message) {
|
||||
if (this._shouldPrintData()) {
|
||||
super.log(...message);
|
||||
}
|
||||
}
|
||||
|
||||
logJSON(o) {
|
||||
if (typeof o === 'string') {
|
||||
console.log(o);
|
||||
} else {
|
||||
console.log(JSON.stringify(o, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* log data in table form. Headers are `[header, key]`
|
||||
* @param {Object} opts
|
||||
* @param {any[]} opts.rows The data to display
|
||||
* @param {string[][]} opts.headers Array of pairs of the column header and the key in the row that that header applies to
|
||||
* @param {string} opts.emptyMessage a message to print if there's no data. Printed in grey
|
||||
* @param {boolean} opts.formatOverride override format and use this instead
|
||||
*/
|
||||
logTable({
|
||||
rows = [],
|
||||
headers = [],
|
||||
emptyMessage = '',
|
||||
formatOverride = '',
|
||||
hasBorder = true,
|
||||
showHeaders = true,
|
||||
style = undefined,
|
||||
} = {}) {
|
||||
const formatter = formatOverride
|
||||
? formatStyles[formatOverride]
|
||||
: formatStyles[this.flags.format];
|
||||
if (!formatter) {
|
||||
// throwing this error ensures that all commands that call this function take a format flag, since that provides the default
|
||||
this.error(`invalid table format: ${this.flags.format}`);
|
||||
}
|
||||
if (!rows.length && this._shouldPrintData()) {
|
||||
this.log(colors.gray(emptyMessage));
|
||||
} else {
|
||||
// data comes out of the formatter ready to be printed (and it's always in the type to match the format) so we don't need to do anything special with it
|
||||
console.log(formatter(rows, headers, showHeaders, hasBorder, style));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} opts options object (as expected for this.prompt())
|
||||
* @returns {string|boolean} Boolean if validation passes, string w/ error message if it doesn't
|
||||
*/
|
||||
_getCustomValidatation(opts) {
|
||||
return (input) => {
|
||||
const validators = {
|
||||
required: (input) =>
|
||||
input.trim() === '' ? 'This field is required.' : true,
|
||||
charLimit: (input, charLimit) =>
|
||||
input.length > charLimit
|
||||
? `Please provide a value ${charLimit} characters or less.`
|
||||
: true,
|
||||
charMinimum: (input, charMinimum) =>
|
||||
input.length < charMinimum
|
||||
? `Please provide a value ${charMinimum} characters or more.`
|
||||
: true,
|
||||
};
|
||||
let aggregateResult = true;
|
||||
|
||||
for (const key in opts) {
|
||||
if (typeof validators[key] === 'undefined') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let individualResult;
|
||||
if (validators[key].length > 1) {
|
||||
individualResult = validators[key](input, opts[key]);
|
||||
} else {
|
||||
individualResult = validators[key](input);
|
||||
}
|
||||
|
||||
if (individualResult !== true) {
|
||||
aggregateResult = individualResult;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return aggregateResult;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* get user input
|
||||
* @param {string} question the question to ask the user
|
||||
* @param {object} opts `inquierer.js` opts ([read more](https://github.com/SBoudrias/Inquirer.js/#question))
|
||||
*/
|
||||
async prompt(question, opts = {}) {
|
||||
if (Object.keys(opts).length) {
|
||||
opts.validate = this._getCustomValidatation(opts);
|
||||
}
|
||||
const prompt = opts.useStderr ? this._stderrPrompt : this._stdoutPrompt;
|
||||
const { ans } = await prompt({
|
||||
type: 'string',
|
||||
...opts,
|
||||
name: 'ans',
|
||||
message: question,
|
||||
});
|
||||
return ans;
|
||||
}
|
||||
|
||||
promptHidden(question, useStderr = false) {
|
||||
return this.prompt(question, {
|
||||
type: 'password',
|
||||
mask: true,
|
||||
useStderr,
|
||||
});
|
||||
}
|
||||
|
||||
confirm(message, defaultAns = false, showCtrlC = false, useStderr = false) {
|
||||
if (showCtrlC) {
|
||||
message += ' (Ctrl-C to cancel)';
|
||||
}
|
||||
return this.prompt(message, {
|
||||
default: defaultAns,
|
||||
type: 'confirm',
|
||||
useStderr,
|
||||
});
|
||||
}
|
||||
|
||||
// see here for options for choices: https://github.com/SBoudrias/Inquirer.js/#question
|
||||
promptWithList(question, choices, additionalOpts) {
|
||||
return this.prompt(question, { type: 'list', choices, ...additionalOpts });
|
||||
}
|
||||
|
||||
/**
|
||||
* should only print to stdout when in a non-data mode
|
||||
*/
|
||||
_shouldPrintData() {
|
||||
return !this.flags.format || !DATA_FORMATS.includes(this.flags.format);
|
||||
}
|
||||
|
||||
startSpinner(message) {
|
||||
startSpinner(message);
|
||||
}
|
||||
|
||||
stopSpinner({ success = true, message = undefined } = {}) {
|
||||
endSpinner(success, message);
|
||||
}
|
||||
|
||||
// pulled from https://github.com/oclif/plugin-help/blob/73bfd5a861e65844a1d6c3a0a9638ee49d16fee8/src/command.ts
|
||||
// renamed to avoid naming collision
|
||||
static zUsage(name) {
|
||||
const formatArg = (arg) => {
|
||||
const argName = arg.name.toUpperCase();
|
||||
return arg.required ? argName : `[${argName}]`;
|
||||
};
|
||||
|
||||
const argv = Object.entries(this.args ?? {}).map(([argName, argValue]) => ({
|
||||
name: argName,
|
||||
...argValue,
|
||||
}));
|
||||
const visibleArgv = argv.filter((arg) => !arg.hidden);
|
||||
|
||||
return ['zapier-platform', name, ...visibleArgv.map(formatArg)].join(' ');
|
||||
}
|
||||
|
||||
// this is fine for now but we'll want to hack into https://github.com/oclif/plugin-help/blob/master/src/command.ts at some point
|
||||
// the presentation is wrapped into the formatting, so it's a little tough to pull out
|
||||
static markdownHelp(name) {
|
||||
const getFormattedArgs = () =>
|
||||
Object.keys(this.args ?? {}).map((argName) => {
|
||||
const arg = this.args[argName];
|
||||
return arg.hidden
|
||||
? null
|
||||
: `* ${arg.required ? '(required) ' : ''}\`${argName}\` | ${
|
||||
arg.description
|
||||
}`;
|
||||
});
|
||||
const getFormattedFlags = () =>
|
||||
Object.entries(this.flags)
|
||||
.map(([flagName, flagValue]) =>
|
||||
flagValue.hidden
|
||||
? null
|
||||
: `* ${flagValue.required ? '(required) ' : ''}\`${
|
||||
flagValue.char ? `-${flagValue.char}, ` : ''
|
||||
}--${flagName}\` |${
|
||||
flagValue.description ? ` ${flagValue.description}` : ''
|
||||
} ${
|
||||
flagValue.options
|
||||
? `One of \`[${flagValue.options.join(' | ')}]\`.`
|
||||
: ''
|
||||
}${
|
||||
flagValue.default
|
||||
? ` Defaults to \`${flagValue.default}\`.`
|
||||
: ''
|
||||
}
|
||||
`.trim(),
|
||||
)
|
||||
.filter(Boolean);
|
||||
|
||||
const descriptionParts = this.description.split('\n\n').filter(Boolean);
|
||||
const blurb = descriptionParts[0];
|
||||
const lengthyDescription = colors.stripColors(
|
||||
descriptionParts.length > 1 ? descriptionParts.slice(1).join('\n\n') : '',
|
||||
);
|
||||
|
||||
return [
|
||||
`## ${name}`,
|
||||
'',
|
||||
`> ${blurb}`,
|
||||
'',
|
||||
`**Usage**: \`${this.zUsage(name)}\``,
|
||||
...(lengthyDescription ? ['', lengthyDescription] : []),
|
||||
...(Object.keys(this.args ?? {}).length
|
||||
? ['', '**Arguments**', ...getFormattedArgs()]
|
||||
: []),
|
||||
...(Object.keys(this.flags ?? {}).length
|
||||
? ['', '**Flags**', ...getFormattedFlags()]
|
||||
: []),
|
||||
...((this.examples ?? []).length
|
||||
? [
|
||||
'',
|
||||
'**Examples**',
|
||||
this.examples.map((e) => `* \`${e}\``).join('\n'),
|
||||
]
|
||||
: []),
|
||||
...((this.aliases ?? []).length
|
||||
? ['', '**Aliases**', this.aliases.map((e) => `* \`${e}\``).join('\n')]
|
||||
: []),
|
||||
]
|
||||
.join('\n')
|
||||
.trim();
|
||||
}
|
||||
|
||||
_recordAnalytics() {
|
||||
// if we got here, the command must be valid
|
||||
if (!this.args) {
|
||||
throw new Error('unable to record analytics until args are parsed');
|
||||
}
|
||||
return recordAnalytics(this.id, true, this.args, this.flags);
|
||||
}
|
||||
}
|
||||
|
||||
ZapierBaseCommand.skipValidInstallCheck = false;
|
||||
|
||||
module.exports = ZapierBaseCommand;
|
||||
43
vendor/zapier-platform/packages/cli/src/oclif/buildFlags.js
vendored
Normal file
43
vendor/zapier-platform/packages/cli/src/oclif/buildFlags.js
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
const { Flags } = require('@oclif/core');
|
||||
const { pickBy } = require('lodash');
|
||||
|
||||
const { formatStyles } = require('../utils/display');
|
||||
|
||||
const baseFlags = {
|
||||
debug: Flags.boolean({
|
||||
char: 'd',
|
||||
description: 'Show extra debugging output.',
|
||||
// pull from env?
|
||||
}),
|
||||
format: Flags.string({
|
||||
char: 'f',
|
||||
options: Object.keys(formatStyles),
|
||||
default: 'table',
|
||||
description:
|
||||
'Change the way structured data is presented. If "json" or "raw", you can pipe the output of the command into other tools, such as jq.',
|
||||
}),
|
||||
// Indicates we're calling a command from another command so we know when not
|
||||
// to print duplicate messages.
|
||||
invokedFromAnotherCommand: Flags.boolean({
|
||||
hidden: true,
|
||||
}),
|
||||
};
|
||||
|
||||
// didn't destruture these opts because I want them all on one object to be picked from
|
||||
const defaultOpts = {
|
||||
debug: true,
|
||||
format: false,
|
||||
invokedFromAnotherCommand: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* pass in flag objects, plus whether or not to include debug, format, and
|
||||
* invokedFormatAnotherCommand.
|
||||
*/
|
||||
const buildFlags = ({ commandFlags = {}, opts = {} } = {}) => {
|
||||
const options = { ...defaultOpts, ...opts };
|
||||
const pickedFlags = pickBy(baseFlags, (_v, k) => options[k]);
|
||||
return { ...commandFlags, ...pickedFlags };
|
||||
};
|
||||
|
||||
module.exports = { buildFlags };
|
||||
47
vendor/zapier-platform/packages/cli/src/oclif/commands/analytics.js
vendored
Normal file
47
vendor/zapier-platform/packages/cli/src/oclif/commands/analytics.js
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const { Flags } = require('@oclif/core');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
const {
|
||||
currentAnalyticsMode,
|
||||
modes,
|
||||
setAnalyticsMode,
|
||||
} = require('../../utils/analytics');
|
||||
const colors = require('colors/safe');
|
||||
|
||||
class AnalyticsCommand extends BaseCommand {
|
||||
async perform() {
|
||||
const currentMode = await currentAnalyticsMode();
|
||||
this.log(
|
||||
`The current analytics mode is ${colors.cyan(
|
||||
currentMode,
|
||||
)}. Analytics may be skipped anyway if you've got DISABLE_ZAPIER_ANALYTICS set to a truthy value.`,
|
||||
);
|
||||
|
||||
if (this.flags.mode) {
|
||||
this.log(`\nSetting analytics mode to ${colors.cyan(this.flags.mode)}.`);
|
||||
return setAnalyticsMode(this.flags.mode);
|
||||
} else {
|
||||
this.log(
|
||||
`You can see what data is sent by running \`${colors.yellow(
|
||||
'DEBUG=zapier:analytics zapier someCommand',
|
||||
)}\`.\n\nYou can change your analytics preferences by re-running this command with the \`--mode\` flag.\n\nThe data collected is as generic as we can make it while still getting useful input. No specific information about your filesystem is collected. Your Zapier user id is collected so that we can better debug issues. We will never use this data for any advertising puposes.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AnalyticsCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
mode: Flags.string({
|
||||
char: 'm',
|
||||
options: Object.keys(modes),
|
||||
description:
|
||||
'Choose how much information to share. Anonymous mode drops the OS type and Zapier user id, but keeps command info. Identifying information is used only for debugging purposes.',
|
||||
}),
|
||||
},
|
||||
});
|
||||
AnalyticsCommand.examples = ['zapier-platform analytics --mode enabled'];
|
||||
AnalyticsCommand.description = `Show the status of the analytics that are collected. Also used to change what is collected.`;
|
||||
AnalyticsCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = AnalyticsCommand;
|
||||
69
vendor/zapier-platform/packages/cli/src/oclif/commands/build.js
vendored
Normal file
69
vendor/zapier-platform/packages/cli/src/oclif/commands/build.js
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const { Flags } = require('@oclif/core');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
const {
|
||||
BUILD_PATH,
|
||||
SOURCE_PATH,
|
||||
CURRENT_APP_FILE,
|
||||
} = require('../../constants');
|
||||
|
||||
const { buildAndOrUpload } = require('../../utils/build');
|
||||
const colors = require('colors/safe');
|
||||
|
||||
class BuildCommand extends BaseCommand {
|
||||
async perform() {
|
||||
const skipDepInstall = this.flags['skip-dep-install'];
|
||||
await buildAndOrUpload(
|
||||
{ build: true },
|
||||
{
|
||||
skipDepInstall,
|
||||
disableDependencyDetection: this.flags['disable-dependency-detection'],
|
||||
skipValidation: this.flags['skip-validation'],
|
||||
},
|
||||
);
|
||||
|
||||
this.log(
|
||||
`\nBuild complete! Created ${BUILD_PATH} and ${SOURCE_PATH}.\n` +
|
||||
`Now you can upload them with the ${colors.bold.underline('zapier-platform upload')} command.`,
|
||||
);
|
||||
|
||||
if (!skipDepInstall) {
|
||||
this.log(
|
||||
`\nTip: Try ${colors.bold.underline('zapier-platform build --skip-dep-install')} for faster builds.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BuildCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
'disable-dependency-detection': Flags.boolean({
|
||||
description: `Disable "smart" file inclusion. By default, Zapier only includes files that are required by your entry point (\`index.js\` by default). If you (or your dependencies) require files dynamically (such as with \`require(someVar)\`), then you may see "Cannot find module" errors. Disabling this may make your \`build.zip\` too large. If that's the case, try using the \`includeInBuild\` option in your \`${CURRENT_APP_FILE}\`. See the docs about \`includeInBuild\` for more info.`,
|
||||
}),
|
||||
'skip-dep-install': Flags.boolean({
|
||||
aliases: ['skip-npm-install'],
|
||||
description:
|
||||
'[alias: --skip-npm-install]\nSkips installing a fresh copy of dependencies for shorter build time. Helpful for using yarn, pnpm, or local copies of dependencies.',
|
||||
}),
|
||||
'skip-validation': Flags.boolean({
|
||||
description:
|
||||
"Skips local pre-push validation checks, and remote validation check of the CLI app's schema and AppVersion integrity.",
|
||||
hidden: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
BuildCommand.description = `Build a pushable zip from the current directory.
|
||||
|
||||
This command does the following:
|
||||
|
||||
* Creates a temporary folder
|
||||
* Copies all code into the temporary folder
|
||||
* Adds an entry point: \`zapierwrapper.js\`
|
||||
* Generates and validates app definition.
|
||||
* Detects dependencies via esbuild (optional, on by default)
|
||||
* Zips up all needed \`.js\` files. If you want to include more files, add a "includeInBuild" property (array with strings of regexp paths) to your \`${CURRENT_APP_FILE}\`.
|
||||
* Moves the zip to \`${BUILD_PATH}\` and \`${SOURCE_PATH}\` and deletes the temp folder
|
||||
|
||||
This command is typically followed by \`zapier-platform upload\`.`;
|
||||
|
||||
module.exports = BuildCommand;
|
||||
116
vendor/zapier-platform/packages/cli/src/oclif/commands/cache/clear.js
vendored
Normal file
116
vendor/zapier-platform/packages/cli/src/oclif/commands/cache/clear.js
vendored
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
const BaseCommand = require('../../ZapierBaseCommand');
|
||||
const { Args } = require('@oclif/core');
|
||||
const { buildFlags } = require('../../buildFlags');
|
||||
const { listVersions, getWritableApp, callAPI } = require('../../../utils/api');
|
||||
const { cyan } = require('colors/safe');
|
||||
|
||||
class ClearCacheCommand extends BaseCommand {
|
||||
async perform() {
|
||||
this.startSpinner('Fetching versions...');
|
||||
const { versions } = await listVersions();
|
||||
this.stopSpinner();
|
||||
|
||||
const { majorVersion } = this.args;
|
||||
|
||||
let selectedMajorVersion = majorVersion ? Number(majorVersion) : null;
|
||||
if (Number.isNaN(selectedMajorVersion)) {
|
||||
throw new Error(
|
||||
`Invalid major version '${majorVersion}'. Must be a number.`,
|
||||
);
|
||||
}
|
||||
|
||||
const majorVersions = [
|
||||
...new Set(
|
||||
versions.map((appVersion) => Number(appVersion.version.split('.')[0])),
|
||||
),
|
||||
];
|
||||
// Finds the current version in package.json.
|
||||
const currentVersion = await require(`${process.cwd()}/package.json`)
|
||||
.version;
|
||||
|
||||
if (selectedMajorVersion === null) {
|
||||
selectedMajorVersion = await this._promptForMajorVersionSelection(
|
||||
majorVersions,
|
||||
currentVersion,
|
||||
);
|
||||
} else {
|
||||
if (!majorVersions.includes(selectedMajorVersion)) {
|
||||
throw new Error(
|
||||
`This integration does not have any versions on major version '${selectedMajorVersion}'. Valid versions are: ${majorVersions.join(
|
||||
', ',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!(await this.confirm(
|
||||
`Are you sure you want to clear all cache data for major version '${cyan(
|
||||
selectedMajorVersion,
|
||||
)}'?`,
|
||||
true,
|
||||
))
|
||||
) {
|
||||
this.log('\ncancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
this.startSpinner('Clearing cache');
|
||||
const { id: appId } = await getWritableApp();
|
||||
const url = `/apps/${appId}/major-versions/${selectedMajorVersion}/cache`;
|
||||
|
||||
await callAPI(url, { method: 'DELETE' });
|
||||
|
||||
this.stopSpinner();
|
||||
|
||||
this.log('Ok! Job is queued.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts user to select a major version from a list of major versions.
|
||||
* @returns { majorVersion: int}
|
||||
*/
|
||||
async _promptForMajorVersionSelection(majorVersions, currentVersion) {
|
||||
const currentMajorVersion = Number(currentVersion.split('.')[0]);
|
||||
|
||||
const majorVersionChoices = majorVersions.map((v) => {
|
||||
const isCurrentMajorVersion = currentMajorVersion === v;
|
||||
|
||||
return {
|
||||
name: `${v}${
|
||||
isCurrentMajorVersion ? ` (current version '${currentVersion}')` : ''
|
||||
}`,
|
||||
value: v,
|
||||
};
|
||||
});
|
||||
|
||||
return await this.promptWithList(
|
||||
"Which major version's cache data would you like to delete?",
|
||||
majorVersionChoices,
|
||||
{ default: currentMajorVersion },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ClearCacheCommand.args = {
|
||||
majorVersion: Args.string({
|
||||
description:
|
||||
'(Optional) The cache data will be deleted for this major version. If not provided, you must pick from a list of major versions for this integration.',
|
||||
required: false,
|
||||
}),
|
||||
};
|
||||
ClearCacheCommand.flags = buildFlags();
|
||||
ClearCacheCommand.description = `Clear the cache data for a major version.
|
||||
|
||||
This command clears the cache data for a major version of your integration.
|
||||
The job will be run in the background and may take some time to complete.
|
||||
You can check \`zapier-platform history\` to see the job status.
|
||||
`;
|
||||
ClearCacheCommand.examples = [
|
||||
`zapier-platform cache:clear`,
|
||||
`zapier-platform cache:clear 2`,
|
||||
];
|
||||
ClearCacheCommand.skipValidInstallCheck = true;
|
||||
ClearCacheCommand.hide = true;
|
||||
|
||||
module.exports = ClearCacheCommand;
|
||||
149
vendor/zapier-platform/packages/cli/src/oclif/commands/canary/create.js
vendored
Normal file
149
vendor/zapier-platform/packages/cli/src/oclif/commands/canary/create.js
vendored
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
const ZapierBaseCommand = require('../../ZapierBaseCommand');
|
||||
const { Args, Flags } = require('@oclif/core');
|
||||
const { createCanary, listCanaries } = require('../../../utils/api');
|
||||
const { buildFlags } = require('../../buildFlags');
|
||||
|
||||
class CanaryCreateCommand extends ZapierBaseCommand {
|
||||
async perform() {
|
||||
const { versionFrom, versionTo } = this.args;
|
||||
const percent = this.flags.percent;
|
||||
const duration = this.flags.duration;
|
||||
const user = this.flags.user;
|
||||
const accountId = this.flags['account-id'];
|
||||
const forceIncludeAll = this.flags['force-include-all'];
|
||||
|
||||
this.validateVersions(versionFrom, versionTo);
|
||||
this.validatePercent(percent);
|
||||
this.validateDuration(duration);
|
||||
|
||||
const activeCanaries = await listCanaries();
|
||||
if (activeCanaries.objects.length > 0) {
|
||||
const existingCanary = activeCanaries.objects[0];
|
||||
const secondsRemaining =
|
||||
existingCanary.until_timestamp - Math.floor(Date.now() / 1000);
|
||||
this
|
||||
.log(`A canary deployment already exists from version ${existingCanary.from_version} to version ${existingCanary.to_version}, there are ${secondsRemaining} seconds remaining.
|
||||
|
||||
If you would like to stop this canary now, run \`zapier-platform canary:delete ${existingCanary.from_version} ${existingCanary.to_version}\``);
|
||||
return;
|
||||
}
|
||||
|
||||
let createCanaryMessage = `Creating canary deployment
|
||||
- From version: ${versionFrom}
|
||||
- To version: ${versionTo}
|
||||
- Percentage: ${percent}%
|
||||
- Duration: ${duration} seconds`;
|
||||
|
||||
const body = {
|
||||
percent,
|
||||
duration,
|
||||
};
|
||||
|
||||
if (user) {
|
||||
body.user = user;
|
||||
createCanaryMessage += `\n - User: ${user}`;
|
||||
}
|
||||
|
||||
if (accountId) {
|
||||
body.account_id = parseInt(accountId);
|
||||
createCanaryMessage += `\n - Account ID: ${accountId}`;
|
||||
}
|
||||
|
||||
if (forceIncludeAll) {
|
||||
body.force_include_all = true;
|
||||
createCanaryMessage += `\n - Force Include All: true`;
|
||||
}
|
||||
|
||||
this.startSpinner(createCanaryMessage);
|
||||
await createCanary(versionFrom, versionTo, body);
|
||||
|
||||
this.stopSpinner();
|
||||
this.log('Canary deployment created successfully.');
|
||||
}
|
||||
|
||||
validateVersions(versionFrom, versionTo) {
|
||||
this.throwForInvalidVersion(versionFrom);
|
||||
this.throwForInvalidVersion(versionTo);
|
||||
|
||||
if (versionFrom === versionTo) {
|
||||
this.error('`VERSIONFROM` and `VERSIONTO` can not be the same');
|
||||
}
|
||||
}
|
||||
|
||||
validatePercent(percent) {
|
||||
if (isNaN(percent) || percent < 1 || percent > 100) {
|
||||
this.error('`--percent` must be a number between 1 and 100');
|
||||
}
|
||||
}
|
||||
|
||||
validateDuration(duration) {
|
||||
if (isNaN(duration) || duration < 30 || duration > 24 * 60 * 60) {
|
||||
this.error('`--duration` must be a positive number between 30 and 86400');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CanaryCreateCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
percent: Flags.integer({
|
||||
char: 'p',
|
||||
description: 'Percent of traffic to route to new version',
|
||||
required: true,
|
||||
}),
|
||||
duration: Flags.integer({
|
||||
char: 'd',
|
||||
description: 'Duration of the canary in seconds',
|
||||
required: true,
|
||||
}),
|
||||
user: Flags.string({
|
||||
char: 'u',
|
||||
description:
|
||||
'Canary this user (email) across all accounts, unless `account-id` is specified.',
|
||||
}),
|
||||
'account-id': Flags.string({
|
||||
char: 'a',
|
||||
description:
|
||||
'The account ID to target. If user is specified, only canary the user within this account. If user is not specified, then this argument is only permitted for Zapier staff.',
|
||||
}),
|
||||
'force-include-all': Flags.boolean({
|
||||
char: 'f',
|
||||
description:
|
||||
'Overrides any default filters the canary system imposes. This argument is only permitted for Zapier staff.',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
CanaryCreateCommand.args = {
|
||||
versionFrom: Args.string({
|
||||
description: 'Version to route traffic from',
|
||||
required: true,
|
||||
}),
|
||||
versionTo: Args.string({
|
||||
description: 'Version to canary traffic to',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
CanaryCreateCommand.description = `Create a new canary deployment, diverting a specified percentage of traffic from one version to another for a specified duration.
|
||||
|
||||
Only one canary can be active at the same time. You can run \`zapier-platform canary:list\` to check. If you would like to create a new canary with different parameters, you can wait for the canary to finish, or delete it using \`zapier-platform canary:delete a.b.c x.y.z\`.
|
||||
|
||||
To canary traffic for a specific user, use the --user flag.
|
||||
|
||||
To canary traffic for an entire account, use the --account-id. Note: this scenario is only permitted for Zapier staff.
|
||||
|
||||
To canary traffic for a specific user within a specific account, use both --user and --account-id flags.
|
||||
|
||||
Note: this is similar to \`zapier-platform migrate\` but different in that this is temporary and will "revert" the changes once the specified duration is expired.
|
||||
|
||||
**Only use this command to canary traffic between non-breaking versions!**`;
|
||||
|
||||
CanaryCreateCommand.examples = [
|
||||
'zapier-platform canary:create 1.0.0 1.1.0 -p 10 -d 3600',
|
||||
'zapier-platform canary:create 2.0.0 2.1.0 --percent 25 --duration 1800 --user user@example.com',
|
||||
'zapier-platform canary:create 2.0.0 2.1.0 -p 15 -d 7200 -a 12345 -u user@example.com',
|
||||
'zapier-platform canary:create 2.0.0 2.1.0 -p 15 -d 7200 -a 12345',
|
||||
];
|
||||
CanaryCreateCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = CanaryCreateCommand;
|
||||
69
vendor/zapier-platform/packages/cli/src/oclif/commands/canary/delete.js
vendored
Normal file
69
vendor/zapier-platform/packages/cli/src/oclif/commands/canary/delete.js
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
const ZapierBaseCommand = require('../../ZapierBaseCommand');
|
||||
const { Args } = require('@oclif/core');
|
||||
const { deleteCanary, listCanaries } = require('../../../utils/api');
|
||||
|
||||
class CanaryDeleteCommand extends ZapierBaseCommand {
|
||||
async perform() {
|
||||
const { versionFrom, versionTo } = this.args;
|
||||
|
||||
this.validateVersions(versionFrom, versionTo);
|
||||
|
||||
const existingCanary = await this.findExistingCanary(
|
||||
versionFrom,
|
||||
versionTo,
|
||||
);
|
||||
if (!existingCanary) {
|
||||
this.log(
|
||||
`There is no active canary from version ${versionFrom} to version ${versionTo}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await this.confirm(
|
||||
`Are you sure you want to delete the canary from ${versionFrom} to ${versionTo}?`,
|
||||
);
|
||||
if (!confirmed) {
|
||||
this.log('Canary deletion cancelled.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.startSpinner(
|
||||
`Deleting active canary from ${versionFrom} to ${versionTo}`,
|
||||
);
|
||||
await deleteCanary(versionFrom, versionTo);
|
||||
this.stopSpinner();
|
||||
this.log('Canary deployment deleted successfully.');
|
||||
}
|
||||
|
||||
async findExistingCanary(versionFrom, versionTo) {
|
||||
const activeCanaries = await listCanaries();
|
||||
return activeCanaries.objects.find(
|
||||
(c) => c.from_version === versionFrom && c.to_version === versionTo,
|
||||
);
|
||||
}
|
||||
|
||||
validateVersions(versionFrom, versionTo) {
|
||||
this.throwForInvalidVersion(versionFrom);
|
||||
this.throwForInvalidVersion(versionTo);
|
||||
|
||||
if (versionFrom === versionTo) {
|
||||
this.error('Versions can not be the same');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CanaryDeleteCommand.args = {
|
||||
versionFrom: Args.string({
|
||||
description: 'Version to route traffic from',
|
||||
required: true,
|
||||
}),
|
||||
versionTo: Args.string({
|
||||
description: 'Version canary traffic is routed to',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
CanaryDeleteCommand.description = 'Delete an active canary deployment';
|
||||
CanaryDeleteCommand.examples = ['zapier-platform canary:delete 1.0.0 1.1.0'];
|
||||
CanaryDeleteCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = CanaryDeleteCommand;
|
||||
40
vendor/zapier-platform/packages/cli/src/oclif/commands/canary/list.js
vendored
Normal file
40
vendor/zapier-platform/packages/cli/src/oclif/commands/canary/list.js
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
const ZapierBaseCommand = require('../../ZapierBaseCommand');
|
||||
const { grey, bold } = require('colors/safe');
|
||||
const { listCanaries } = require('../../../utils/api');
|
||||
const { buildFlags } = require('../../buildFlags');
|
||||
|
||||
class CanaryListCommand extends ZapierBaseCommand {
|
||||
async perform() {
|
||||
const canaries = await listCanaries();
|
||||
|
||||
const formattedCanaries = canaries.objects.map((c) => ({
|
||||
from_version: c.from_version,
|
||||
to_version: c.to_version,
|
||||
percent: c.percent,
|
||||
seconds_remaining: c.until_timestamp - Math.floor(Date.now() / 1000),
|
||||
user: c.user,
|
||||
account_id: c.account_id,
|
||||
}));
|
||||
|
||||
this.log(bold('Active Canaries') + '\n');
|
||||
this.logTable({
|
||||
rows: formattedCanaries,
|
||||
headers: [
|
||||
['From Version', 'from_version'],
|
||||
['To Version', 'to_version'],
|
||||
['Traffic Amount', 'percent'],
|
||||
['Seconds Remaining', 'seconds_remaining'],
|
||||
['User', 'user'],
|
||||
['Account ID', 'account_id'],
|
||||
],
|
||||
emptyMessage: grey(`No active canary deployments found.`),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
CanaryListCommand.flags = buildFlags({ opts: { format: true } });
|
||||
CanaryListCommand.description = 'List all active canary deployments';
|
||||
CanaryListCommand.examples = ['zapier-platform canary:list'];
|
||||
CanaryListCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = CanaryListCommand;
|
||||
163
vendor/zapier-platform/packages/cli/src/oclif/commands/convert.js
vendored
Normal file
163
vendor/zapier-platform/packages/cli/src/oclif/commands/convert.js
vendored
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
const fs = require('node:fs/promises');
|
||||
|
||||
const { Args, Flags } = require('@oclif/core');
|
||||
|
||||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
|
||||
const { callAPI } = require('../../utils/api');
|
||||
const { convertApp } = require('../../utils/convert');
|
||||
const { isExistingEmptyDir } = require('../../utils/files');
|
||||
const { initApp } = require('../../utils/init');
|
||||
|
||||
const readStream = async (stream) => {
|
||||
const chunks = [];
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
return chunks.join('');
|
||||
};
|
||||
|
||||
class ConvertCommand extends BaseCommand {
|
||||
generateCreateFunc(appId, version, json, title, description) {
|
||||
return async (tempAppDir) => {
|
||||
if (json) {
|
||||
const appInfo = {
|
||||
title,
|
||||
description,
|
||||
};
|
||||
|
||||
let parsedDefinition = json;
|
||||
if (parsedDefinition.startsWith('@')) {
|
||||
const filePath = parsedDefinition.substr(1);
|
||||
let definitionStream;
|
||||
if (filePath === '-') {
|
||||
definitionStream = process.stdin;
|
||||
} else {
|
||||
const fd = await fs.open(filePath);
|
||||
definitionStream = fd.createReadStream({ encoding: 'utf8' });
|
||||
}
|
||||
parsedDefinition = await readStream(definitionStream);
|
||||
}
|
||||
parsedDefinition = JSON.parse(parsedDefinition);
|
||||
|
||||
return convertApp(appInfo, parsedDefinition, tempAppDir);
|
||||
}
|
||||
|
||||
// has info about the app, such as title
|
||||
// has a CLI version of the actual app implementation
|
||||
this.throwForInvalidVersion(version);
|
||||
this.startSpinner('Downloading integration from Zapier');
|
||||
try {
|
||||
const [appInfo, versionInfo] = await Promise.all([
|
||||
callAPI(`/apps/${appId}`, undefined, true),
|
||||
callAPI(`/apps/${appId}/versions/${version}`, undefined, true),
|
||||
]);
|
||||
|
||||
if (!versionInfo.definition_override) {
|
||||
this.error(
|
||||
`Integration ${appId} @ ${version} is already a CLI integration and can't be converted. Instead, pick a version that was created using the Visual Builder.`,
|
||||
);
|
||||
}
|
||||
this.stopSpinner();
|
||||
|
||||
return convertApp(appInfo, versionInfo.definition_override, tempAppDir);
|
||||
} catch (e) {
|
||||
if (e.status === 404) {
|
||||
this.error(
|
||||
`Visual Builder integration ${appId} @ ${version} not found. Double check the integration id and version.`,
|
||||
);
|
||||
}
|
||||
this.error(e.json.errors[0]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async perform() {
|
||||
const { path } = this.args;
|
||||
const {
|
||||
integrationId: appId,
|
||||
version,
|
||||
json,
|
||||
title,
|
||||
description,
|
||||
} = this.flags;
|
||||
|
||||
if (
|
||||
(await isExistingEmptyDir(path)) &&
|
||||
!(await this.confirm(`Path "${path}" is not empty. Continue anyway?`))
|
||||
) {
|
||||
this.exit();
|
||||
}
|
||||
|
||||
if (!appId && !json) {
|
||||
this.error('You must provide either an integrationId or json.');
|
||||
}
|
||||
|
||||
await initApp(
|
||||
path,
|
||||
this.generateCreateFunc(appId, version, json, title, description),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ConvertCommand.args = {
|
||||
path: Args.string({
|
||||
description:
|
||||
'Relative to your current path - IE: `.` for current directory.',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
ConvertCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
integrationId: Args.string({
|
||||
char: 'i',
|
||||
description: `To get the integration/app ID, go to "https://developer.zapier.com", click on an integration, and copy the number directly after "/app/" in the URL.`,
|
||||
required: false,
|
||||
dependsOn: ['version'],
|
||||
exclusive: ['definition'],
|
||||
parse: (input) => Number(input),
|
||||
}),
|
||||
version: Flags.string({
|
||||
char: 'v',
|
||||
description:
|
||||
'Convert a specific version. Required when converting a Visual Builder integration.',
|
||||
required: false,
|
||||
dependsOn: ['integrationId'],
|
||||
}),
|
||||
json: Flags.string({
|
||||
char: 'j',
|
||||
description:
|
||||
'The JSON definition to use, as alternative for reading from a Visual Builder integration. 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 @-.',
|
||||
required: false,
|
||||
exclusive: ['integrationId'],
|
||||
}),
|
||||
title: Flags.string({
|
||||
char: 't',
|
||||
description:
|
||||
'The integration title, which will be snake-cased for the package.json name.',
|
||||
required: false,
|
||||
dependsOn: ['json'],
|
||||
}),
|
||||
description: Flags.string({
|
||||
char: 'd',
|
||||
description:
|
||||
'The integration description, which will be used for the package.json description.',
|
||||
required: false,
|
||||
dependsOn: ['json'],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
ConvertCommand.description = `Convert a Visual Builder integration to a CLI integration.
|
||||
|
||||
The resulting CLI integration will be identical to its Visual Builder version and ready to push and use immediately!
|
||||
|
||||
If you re-run this command on an existing directory it will leave existing files alone and not clobber them.
|
||||
|
||||
You'll need to do a \`zapier-platform push\` before the new version is visible in the editor, but otherwise you're good to go.`;
|
||||
|
||||
ConvertCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = ConvertCommand;
|
||||
24
vendor/zapier-platform/packages/cli/src/oclif/commands/delete/integration.js
vendored
Normal file
24
vendor/zapier-platform/packages/cli/src/oclif/commands/delete/integration.js
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
const BaseCommand = require('../../ZapierBaseCommand');
|
||||
const { buildFlags } = require('../../buildFlags');
|
||||
const { callAPI } = require('../../../utils/api');
|
||||
|
||||
class DeleteAppCommand extends BaseCommand {
|
||||
async perform() {
|
||||
const { id, title } = await this.getWritableApp();
|
||||
|
||||
this.startSpinner(`Deleting "${title}"`);
|
||||
await callAPI(`/apps/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
this.stopSpinner();
|
||||
}
|
||||
}
|
||||
|
||||
DeleteAppCommand.flags = buildFlags();
|
||||
DeleteAppCommand.description = `Delete your integration (including all versions).
|
||||
|
||||
This only works if there are no active users or Zaps on any version. If you only want to delete certain versions, use the \`zapier-platform delete:version\` command instead. It's unlikely that you'll be able to run this on an app that you've pushed publicly, since there are usually still users.`;
|
||||
DeleteAppCommand.aliases = ['delete:app'];
|
||||
DeleteAppCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = DeleteAppCommand;
|
||||
32
vendor/zapier-platform/packages/cli/src/oclif/commands/delete/version.js
vendored
Normal file
32
vendor/zapier-platform/packages/cli/src/oclif/commands/delete/version.js
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
const BaseCommand = require('../../ZapierBaseCommand');
|
||||
const { Args } = require('@oclif/core');
|
||||
const { buildFlags } = require('../../buildFlags');
|
||||
const { callAPI } = require('../../../utils/api');
|
||||
|
||||
class DeleteVersionCommand extends BaseCommand {
|
||||
async perform() {
|
||||
const { version } = this.args;
|
||||
|
||||
const { id, title } = await this.getWritableApp();
|
||||
|
||||
this.startSpinner(`Deleting version ${version} of app "${title}"`);
|
||||
await callAPI(`/apps/${id}/versions/${version}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
this.stopSpinner();
|
||||
}
|
||||
}
|
||||
|
||||
DeleteVersionCommand.args = {
|
||||
version: Args.string({
|
||||
description: `Specify the version to delete. It must have no users or Zaps.`,
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
DeleteVersionCommand.flags = buildFlags();
|
||||
DeleteVersionCommand.skipValidInstallCheck = true;
|
||||
DeleteVersionCommand.description = `Delete a specific version of your integration.
|
||||
|
||||
This only works if there are no users or Zaps on that version. You will probably need to have run \`zapier-platform migrate\` and \`zapier-platform deprecate\` before this command will work.`;
|
||||
|
||||
module.exports = DeleteVersionCommand;
|
||||
143
vendor/zapier-platform/packages/cli/src/oclif/commands/deprecate.js
vendored
Normal file
143
vendor/zapier-platform/packages/cli/src/oclif/commands/deprecate.js
vendored
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const { Args, Flags } = require('@oclif/core');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
const colors = require('colors/safe');
|
||||
|
||||
const { callAPI, getSpecificVersionInfo } = require('../../utils/api');
|
||||
|
||||
const DEPRECATION_REASONS = [
|
||||
{ name: 'API endpoint deprecated', value: 'api endpoint deprecated' },
|
||||
{ name: 'Security vulnerability', value: 'security vulnerability' },
|
||||
{ name: 'Critical bug', value: 'critical bug' },
|
||||
{ name: 'Legal requirement', value: 'legal requirement' },
|
||||
{ name: 'Other', value: 'other' },
|
||||
];
|
||||
|
||||
class DeprecateCommand extends BaseCommand {
|
||||
async perform() {
|
||||
const app = await this.getWritableApp();
|
||||
const { version, date } = this.args;
|
||||
|
||||
const versionInfo = await getSpecificVersionInfo(version);
|
||||
const hasActiveUsers = versionInfo.user_count && versionInfo.user_count > 0;
|
||||
|
||||
this.log(
|
||||
`${colors.yellow('Warning: Deprecation is an irreversible action that will eventually block access to this version.')}\n` +
|
||||
`${colors.yellow('If all your changes are non-breaking, use `zapier-platform migrate` instead to move users over to a newer version.')}\n`,
|
||||
);
|
||||
|
||||
// Get deprecation reason - either from flag or prompt user
|
||||
let deprecationReason = this.flags.reason;
|
||||
|
||||
if (!deprecationReason) {
|
||||
deprecationReason = await this.promptWithList(
|
||||
'Please select a reason for deprecating this version:',
|
||||
DEPRECATION_REASONS,
|
||||
{
|
||||
useStderr: true,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Validate the provided reason
|
||||
const validReasons = DEPRECATION_REASONS.map((r) => r.value);
|
||||
if (!validReasons.includes(deprecationReason)) {
|
||||
this.error(
|
||||
`Invalid deprecation reason: ${deprecationReason}. Valid options are: ${validReasons.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let customReason = null;
|
||||
if (deprecationReason === 'other') {
|
||||
customReason = await this.prompt(
|
||||
'Please provide a brief user-facing reason (50 characters max):',
|
||||
{
|
||||
required: true,
|
||||
charLimit: 50,
|
||||
useStderr: true,
|
||||
},
|
||||
);
|
||||
customReason = 'other: ' + customReason;
|
||||
}
|
||||
|
||||
if (
|
||||
!this.flags.force &&
|
||||
!(await this.confirm(
|
||||
'Are you sure you want to deprecate this version? Only do so if it would start to fail otherwise. We will notify users that their Zaps or other automations will stop working two weeks before the specified date.' +
|
||||
(hasActiveUsers
|
||||
? `\n\nThis version has ${versionInfo.user_count} active user(s) via Zaps. Strongly consider migrating users to another version at least two weeks before the deprecation date!`
|
||||
: ''),
|
||||
))
|
||||
) {
|
||||
this.log('\nDeprecation cancelled.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(
|
||||
`\nPreparing to deprecate version ${version} your app "${app.title}" due to: ${customReason || DEPRECATION_REASONS.find((r) => r.value === deprecationReason)?.name}.\n`,
|
||||
);
|
||||
|
||||
const url = `/apps/${app.id}/versions/${version}/deprecate`;
|
||||
this.startSpinner(`Deprecating ${version}`);
|
||||
await callAPI(url, {
|
||||
method: 'PUT',
|
||||
body: {
|
||||
deprecation_date: date,
|
||||
deprecation_reason: customReason || deprecationReason,
|
||||
},
|
||||
});
|
||||
this.stopSpinner();
|
||||
this.log(
|
||||
`\nWe'll let users know that this version will cease to work on ${date}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeprecateCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
force: Flags.boolean({
|
||||
char: 'f',
|
||||
description: 'Skip confirmation prompt. Use with caution.',
|
||||
}),
|
||||
reason: Flags.string({
|
||||
char: 'r',
|
||||
description: 'Reason for deprecation.',
|
||||
options: DEPRECATION_REASONS.map((r) => r.value),
|
||||
}),
|
||||
},
|
||||
});
|
||||
DeprecateCommand.args = {
|
||||
version: Args.string({
|
||||
description: 'The version to deprecate.',
|
||||
required: true,
|
||||
}),
|
||||
date: Args.string({
|
||||
description:
|
||||
'The date (YYYY-MM-DD) when Zapier will make the specified version unavailable.',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
DeprecateCommand.examples = [
|
||||
'zapier-platform deprecate 1.2.3 2011-10-01',
|
||||
'zapier-platform deprecate 1.2.3 2011-10-01 --reason=security_vulnerability',
|
||||
'zapier-platform deprecate 1.2.3 2011-10-01 -r critical_bug',
|
||||
];
|
||||
DeprecateCommand.description = `Mark a non-production version of your integration as deprecated, with removal by a certain date.
|
||||
|
||||
Use this when an integration version will not be supported or start breaking at a known date.
|
||||
|
||||
When deprecating a version, you must provide a reason for the deprecation. You can either specify the reason using the --reason flag or you will be prompted to select from the following options:
|
||||
${DEPRECATION_REASONS.map((r) => `- ${r.name}`).join('\n')}
|
||||
|
||||
The deprecation date must be at least 3 weeks days in the future. Zapier will send emails warning users of the deprecation exactly 14 days before the configured deprecation date. This gives you 1 week to migrate users to a newer version, if possible, before we notify them that they need to do so themselves.
|
||||
|
||||
There are other side effects: they'll start seeing it as "Deprecated" in the UI, and once the deprecation date arrives, if the Zaps weren't updated, they'll be paused and the users will be emailed again explaining what happened.
|
||||
|
||||
Do not use deprecation if you only have non-breaking changes, such as:
|
||||
- Fixing help text
|
||||
- Adding new triggers/actions
|
||||
- Improving existing functionality
|
||||
- other bug fixes that don't break existing automations.`;
|
||||
DeprecateCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = DeprecateCommand;
|
||||
208
vendor/zapier-platform/packages/cli/src/oclif/commands/describe.js
vendored
Normal file
208
vendor/zapier-platform/packages/cli/src/oclif/commands/describe.js
vendored
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
const { bold, grey } = require('colors/safe');
|
||||
const {
|
||||
getWritableApp,
|
||||
getLinkedAppConfig,
|
||||
getVersionInfo,
|
||||
} = require('../../utils/api');
|
||||
const { localAppCommand } = require('../../utils/local');
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
const authenticationPaths = [
|
||||
'authentication.test',
|
||||
'authentication.oauth2Config.getAccessToken',
|
||||
'authentication.oauth2Config.refreshAccessToken',
|
||||
'authentication.sessionConfig.perform',
|
||||
];
|
||||
|
||||
// {type:triggers}.{key:lead}.operation.perform
|
||||
const actionTemplates = [
|
||||
'<%= type %>.<%= key %>.operation.perform',
|
||||
'<%= type %>.<%= key %>.operation.performSubscribe',
|
||||
'<%= type %>.<%= key %>.operation.performUnsubscribe',
|
||||
'<%= type %>.<%= key %>.operation.inputFields',
|
||||
'<%= type %>.<%= key %>.operation.outputFields',
|
||||
].map((template) => _.template(template));
|
||||
|
||||
const hydrateTemplate = _.template('hydrators.<%= key %>');
|
||||
|
||||
const inlineResourceMethods = [
|
||||
'get',
|
||||
'hook',
|
||||
'list',
|
||||
'search',
|
||||
'create',
|
||||
'searchOrCreate',
|
||||
];
|
||||
|
||||
// resources.{key:lead}.get.operation.perform
|
||||
const makeResourceTemplates = (methods) =>
|
||||
methods
|
||||
.reduce((acc, method) => {
|
||||
return acc.concat([
|
||||
`resources.<%= key %>.${method}.operation.perform`,
|
||||
`resources.<%= key %>.${method}.operation.performSubscribe`,
|
||||
`resources.<%= key %>.${method}.operation.performUnsubscribe`,
|
||||
`resources.<%= key %>.${method}.operation.inputFields`,
|
||||
`resources.<%= key %>.${method}.operation.outputFields`,
|
||||
]);
|
||||
}, [])
|
||||
.map((template) => _.template(template));
|
||||
|
||||
const allResourceTemplates = makeResourceTemplates(inlineResourceMethods);
|
||||
|
||||
const typeMap = {
|
||||
triggers: ['list', 'hook'],
|
||||
searches: ['search'],
|
||||
creates: ['create'],
|
||||
};
|
||||
|
||||
class DescribeCommand extends BaseCommand {
|
||||
logTitle(s) {
|
||||
this.log(bold(s) + '\n');
|
||||
}
|
||||
|
||||
async perform() {
|
||||
this.startSpinner('Fetching integration info');
|
||||
const [app, appConfig, version, definition] = await Promise.all([
|
||||
getWritableApp().catch(() => null),
|
||||
getLinkedAppConfig().catch(() => null),
|
||||
getVersionInfo().catch(() => null),
|
||||
localAppCommand({ command: 'definition' }),
|
||||
]);
|
||||
this.stopSpinner();
|
||||
if (app) {
|
||||
this.logTitle('Title');
|
||||
this.log(app.title + '\n');
|
||||
|
||||
if (app.description) {
|
||||
this.logTitle('Description');
|
||||
this.log(app.description + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
this.logTitle('Authentication');
|
||||
let authRows = [];
|
||||
if (definition.authentication) {
|
||||
const authentication = { ...definition.authentication };
|
||||
authentication.paths = authenticationPaths
|
||||
.filter((path) => _.has(definition, path))
|
||||
.join('\n');
|
||||
if (['oauth2', 'oauth1'].includes(authentication.type)) {
|
||||
if (appConfig && version) {
|
||||
authentication.redirect_uri = version.oauth_redirect_uri;
|
||||
} else {
|
||||
authentication.redirect_uri = grey(
|
||||
'Run `zapier-platform push` to see the redirect_uri.',
|
||||
);
|
||||
}
|
||||
}
|
||||
authRows = [authentication];
|
||||
}
|
||||
this.logTable({
|
||||
rows: authRows,
|
||||
headers: [
|
||||
['Type', 'type'],
|
||||
['Redirect URI', 'redirect_uri', grey('n/a')],
|
||||
['Available Methods', 'paths', grey('n/a')],
|
||||
],
|
||||
emptyMessage: grey('No authentication found.'),
|
||||
});
|
||||
this.log();
|
||||
|
||||
const hydratorRows = _.map(definition.hydrators, (val, key) => ({
|
||||
key,
|
||||
paths: hydrateTemplate({ key }),
|
||||
}));
|
||||
this.logTitle('Hydrators');
|
||||
this.logTable({
|
||||
rows: hydratorRows,
|
||||
headers: [
|
||||
['Key', 'key'],
|
||||
['Method', 'paths', grey('n/a')],
|
||||
],
|
||||
emptyMessage: grey('No hydrators found.'),
|
||||
});
|
||||
this.log();
|
||||
|
||||
const resourceRows = _.values(definition.resources || {}).map(
|
||||
(resource) => ({
|
||||
...resource,
|
||||
paths: allResourceTemplates
|
||||
.map((method) => method({ key: resource.key }))
|
||||
.filter((path) => _.has(definition, path))
|
||||
.join('\n'),
|
||||
}),
|
||||
);
|
||||
this.logTitle('Resources');
|
||||
this.logTable({
|
||||
rows: resourceRows,
|
||||
headers: [
|
||||
['Noun', 'noun'],
|
||||
['Ref', 'key'],
|
||||
['Available Methods', 'paths', grey('n/a')],
|
||||
],
|
||||
emptyMessage: grey('No resources found.'),
|
||||
});
|
||||
this.log();
|
||||
|
||||
Object.keys(typeMap).forEach((type) => {
|
||||
this.logTitle(_.capitalize(type));
|
||||
const rows = _.values(definition[type]).map((row) => {
|
||||
// add possible action paths
|
||||
let paths = actionTemplates.map((method) =>
|
||||
method({ type, key: row.key }),
|
||||
);
|
||||
|
||||
// add possible resource paths
|
||||
if (row.operation.resource) {
|
||||
const key = row.operation.resource.split('.')[0];
|
||||
const resourceTemplates = makeResourceTemplates(typeMap[type]);
|
||||
paths = paths.concat(
|
||||
resourceTemplates.map((method) => method({ key })),
|
||||
);
|
||||
}
|
||||
|
||||
paths = paths.filter((path) => _.has(definition, path)).join('\n');
|
||||
|
||||
return {
|
||||
...row,
|
||||
paths,
|
||||
};
|
||||
});
|
||||
|
||||
this.logTable({
|
||||
rows,
|
||||
headers: [
|
||||
['Noun', 'noun'],
|
||||
['Label', 'display.label'],
|
||||
['Resource Ref', 'operation.resource', grey('n/a')],
|
||||
['Available Methods', 'paths', grey('n/a')],
|
||||
],
|
||||
emptyMessage: grey(
|
||||
`Nothing found for ${type}. Use the \`zapier-platform scaffold\` command to add one.`,
|
||||
),
|
||||
});
|
||||
|
||||
this.log();
|
||||
|
||||
this.log('To add more, use the `zapier-platform scaffold` command.');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
DescribeCommand.flags = buildFlags({ opts: { format: true } });
|
||||
DescribeCommand.description = `Describe the current integration.
|
||||
|
||||
This command prints a human readable enumeration of your integrations's
|
||||
triggers, searches, and creates as seen by Zapier. Useful to understand how your
|
||||
resources convert and relate to different actions.
|
||||
|
||||
* **Noun**: your action's noun
|
||||
* **Label**: your action's label
|
||||
* **Resource**: the resource (if any) this action is tied to
|
||||
* **Available Methods**: testable methods for this action`;
|
||||
|
||||
module.exports = DescribeCommand;
|
||||
37
vendor/zapier-platform/packages/cli/src/oclif/commands/env/get.js
vendored
Normal file
37
vendor/zapier-platform/packages/cli/src/oclif/commands/env/get.js
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
const BaseCommand = require('../../ZapierBaseCommand');
|
||||
const { Args } = require('@oclif/core');
|
||||
const { buildFlags } = require('../../buildFlags');
|
||||
const { listEnv } = require('../../../utils/api');
|
||||
|
||||
class GetEnvCommand extends BaseCommand {
|
||||
async perform() {
|
||||
const { version } = this.args;
|
||||
this.throwForInvalidVersion(version);
|
||||
|
||||
const { env } = await listEnv(version);
|
||||
|
||||
this.logTable({
|
||||
rows: env,
|
||||
headers: [
|
||||
['Key', 'key'],
|
||||
['Value', 'value'],
|
||||
],
|
||||
emptyMessage: `Version ${version} has no environment values set`,
|
||||
});
|
||||
|
||||
this.log('Set new values with `zapier-platform env:set`');
|
||||
}
|
||||
}
|
||||
|
||||
GetEnvCommand.args = {
|
||||
version: Args.string({
|
||||
description: 'The version to get the environment for.',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
GetEnvCommand.flags = buildFlags({ opts: { format: true } });
|
||||
GetEnvCommand.description = `Get environment variables for a version.`;
|
||||
GetEnvCommand.examples = [`zapier-platform env:get 1.2.3`];
|
||||
GetEnvCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = GetEnvCommand;
|
||||
113
vendor/zapier-platform/packages/cli/src/oclif/commands/env/set.js
vendored
Normal file
113
vendor/zapier-platform/packages/cli/src/oclif/commands/env/set.js
vendored
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
const { Args, Flags } = require('@oclif/core');
|
||||
const { cyan } = require('colors/safe');
|
||||
const { omit } = require('lodash');
|
||||
|
||||
const BaseCommand = require('../../ZapierBaseCommand');
|
||||
const { buildFlags } = require('../../buildFlags');
|
||||
const { callAPI } = require('../../../utils/api');
|
||||
|
||||
const successMessage = (version) =>
|
||||
`Successfully wrote the following to the environment of version ${cyan(
|
||||
version,
|
||||
)}:`;
|
||||
|
||||
class SetEnvCommand extends BaseCommand {
|
||||
async perform() {
|
||||
const { version } = this.args;
|
||||
this.throwForInvalidVersion(version);
|
||||
// args should be [ '1.0.0', 'qer=123', 'qwer=123' ]
|
||||
const valuesToSet = this.argv.slice(1).filter((kv) => !kv.startsWith('-'));
|
||||
|
||||
if (!valuesToSet.length) {
|
||||
this.error(
|
||||
'Must specify at least one key-value pair to set (like `SOME_KEY=1234`)',
|
||||
);
|
||||
}
|
||||
|
||||
if (!valuesToSet.every((kv) => kv.includes('='))) {
|
||||
this.error('Every key-value pair must be in the format `SOME_KEY=1234`');
|
||||
}
|
||||
|
||||
// if we get here, we should have well-formed input
|
||||
|
||||
const payload = valuesToSet.reduce((result, kvPair) => {
|
||||
const [key, ...valueParts] = kvPair.split('=');
|
||||
const value = valueParts.join('='); // Guards against values with = characters
|
||||
result[key.toUpperCase()] = value;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
const app = await this.getWritableApp();
|
||||
if (!app.all_versions.includes(version)) {
|
||||
this.error(
|
||||
`Version ${version} doesn't exist on integration "${app.title}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const url = `/apps/${app.id}/versions/${version}/multi-environment`;
|
||||
const requestOptions = {
|
||||
body: payload,
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
if (this.flags.force) {
|
||||
requestOptions.extraHeaders = {
|
||||
'X-Force-Env-Var-Update': 'true',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await callAPI(url, requestOptions, true);
|
||||
|
||||
this.log(successMessage(version));
|
||||
this.logJSON(payload);
|
||||
} catch (e) {
|
||||
if (e.status === 409) {
|
||||
this.error(
|
||||
`App version ${version} is the production version. Are you sure you want to set potentially live environment variables?` +
|
||||
` If so, run this command again with the --force flag.`,
|
||||
);
|
||||
}
|
||||
|
||||
// comes back as json: { errors: [ 'The following keys failed to update: 3QER, 4WER' ] },
|
||||
const failedKeys = e.json.errors[0].split('update: ')[1].split(', ');
|
||||
const successfulResult = omit(payload, failedKeys);
|
||||
if (!Object.keys(successfulResult).length) {
|
||||
this.error(e.json.errors.join('\nError: '));
|
||||
}
|
||||
|
||||
this.warn(successMessage(version));
|
||||
this.logJSON(successfulResult);
|
||||
this.warn(e.json.errors.join('\nWarning: '));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SetEnvCommand.args = {
|
||||
version: Args.string({
|
||||
description:
|
||||
'The version to set the environment for. Values are copied forward when a new version is created, but this command will only ever affect the specified version.',
|
||||
required: true,
|
||||
}),
|
||||
'key-value pairs...': Args.string({
|
||||
description:
|
||||
'The key-value pairs to set. Keys are case-insensitive. Each pair should be space separated and pairs should be separated by an `=`. For example: `A=123 B=456`',
|
||||
}),
|
||||
};
|
||||
SetEnvCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
force: Flags.boolean({
|
||||
char: 'f',
|
||||
description:
|
||||
'Force the update of environment variables regardless if the app version is production or not. Use with caution.',
|
||||
}),
|
||||
},
|
||||
});
|
||||
SetEnvCommand.description = `Set environment variables for a version.`;
|
||||
SetEnvCommand.examples = [
|
||||
`zapier-platform env:set 1.2.3 SECRET=12345 OTHER=4321`,
|
||||
];
|
||||
SetEnvCommand.strict = false;
|
||||
SetEnvCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = SetEnvCommand;
|
||||
97
vendor/zapier-platform/packages/cli/src/oclif/commands/env/unset.js
vendored
Normal file
97
vendor/zapier-platform/packages/cli/src/oclif/commands/env/unset.js
vendored
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
const { Args, Flags } = require('@oclif/core');
|
||||
const { cyan } = require('colors/safe');
|
||||
|
||||
const BaseCommand = require('../../ZapierBaseCommand');
|
||||
const { buildFlags } = require('../../buildFlags');
|
||||
const { callAPI } = require('../../../utils/api');
|
||||
|
||||
const successMessage = (version) =>
|
||||
`Successfully unset the following keys in the environment of version ${cyan(
|
||||
version,
|
||||
)} (if they existed):`;
|
||||
|
||||
class UnsetEnvCommand extends BaseCommand {
|
||||
async perform() {
|
||||
const { version } = this.args;
|
||||
this.throwForInvalidVersion(version);
|
||||
// args should be [ '1.0.0', 'qer=123', 'qwer=123' ]
|
||||
const keysToUnset = this.argv
|
||||
.slice(1)
|
||||
.filter((k) => !k.startsWith('-'))
|
||||
.map((k) => k.toUpperCase());
|
||||
|
||||
if (!keysToUnset.length) {
|
||||
this.error('Must specify at least one key to unset (like `SOME_KEY`)');
|
||||
}
|
||||
|
||||
if (keysToUnset.some((v) => v.includes('='))) {
|
||||
this.error('Do not specify values using the unset operation, only keys');
|
||||
}
|
||||
|
||||
// if we get here, we should have well-formed input
|
||||
const payload = keysToUnset.reduce((result, key) => {
|
||||
result[key] = null;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
const app = await this.getWritableApp();
|
||||
if (!app.all_versions.includes(version)) {
|
||||
this.error(
|
||||
`Version ${version} doesn't exist on integration "${app.title}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const url = `/apps/${app.id}/versions/${version}/multi-environment`;
|
||||
const requestOptions = {
|
||||
body: payload,
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
if (this.flags.force) {
|
||||
requestOptions.extraHeaders = {
|
||||
'X-Force-Env-Var-Update': 'true',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await callAPI(url, requestOptions, true);
|
||||
} catch (e) {
|
||||
if (e.status === 409) {
|
||||
this.error(
|
||||
`App version ${version} is the production version. Are you sure you want to unset potentially live environment variables?` +
|
||||
` If so, run this command again with the --force flag.`,
|
||||
);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
this.log(successMessage(version));
|
||||
this.logJSON(keysToUnset);
|
||||
}
|
||||
}
|
||||
|
||||
UnsetEnvCommand.args = {
|
||||
version: Args.string({
|
||||
description: 'The version to set the environment for.',
|
||||
required: true,
|
||||
}),
|
||||
'keys...': Args.string({
|
||||
description: 'The keys to unset. Keys are case-insensitive.',
|
||||
}),
|
||||
};
|
||||
UnsetEnvCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
force: Flags.boolean({
|
||||
char: 'f',
|
||||
description:
|
||||
'Force the update of environment variables regardless if the app version is production or not. Use with caution.',
|
||||
}),
|
||||
},
|
||||
});
|
||||
UnsetEnvCommand.description = `Unset environment variables for a version.`;
|
||||
UnsetEnvCommand.examples = [`zapier-platform env:unset 1.2.3 SECRET OTHER`];
|
||||
UnsetEnvCommand.strict = false;
|
||||
UnsetEnvCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = UnsetEnvCommand;
|
||||
32
vendor/zapier-platform/packages/cli/src/oclif/commands/history.js
vendored
Normal file
32
vendor/zapier-platform/packages/cli/src/oclif/commands/history.js
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
|
||||
const { listHistory } = require('../../utils/api');
|
||||
|
||||
class HistoryCommand extends BaseCommand {
|
||||
async perform() {
|
||||
this.startSpinner('Loading history');
|
||||
const { history } = await listHistory();
|
||||
this.stopSpinner();
|
||||
|
||||
this.logTable({
|
||||
rows: history,
|
||||
headers: [
|
||||
['What', 'action'],
|
||||
['Message', 'message'],
|
||||
['Who', 'customuser'],
|
||||
['Version', 'version'],
|
||||
['Timestamp', 'date'],
|
||||
],
|
||||
emptyMessage: 'No historical actions found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
HistoryCommand.skipValidInstallCheck = true;
|
||||
HistoryCommand.flags = buildFlags({ opts: { format: true } });
|
||||
HistoryCommand.description = `Get the history of your integration.
|
||||
|
||||
History includes all the changes made over the lifetime of your integration. This includes everything from creation, updates, migrations, admins, and invitee changes, as well as who made the change and when.`;
|
||||
|
||||
module.exports = HistoryCommand;
|
||||
72
vendor/zapier-platform/packages/cli/src/oclif/commands/init.js
vendored
Normal file
72
vendor/zapier-platform/packages/cli/src/oclif/commands/init.js
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
const { join } = require('path');
|
||||
|
||||
const { Args, Flags } = require('@oclif/core');
|
||||
const { createEnv } = require('../../utils/esm-wrapper');
|
||||
|
||||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
const {
|
||||
TEMPLATE_CHOICES,
|
||||
ProjectGenerator: ProjectGeneratorPromise,
|
||||
} = require('../../generators');
|
||||
|
||||
class InitCommand extends BaseCommand {
|
||||
async perform() {
|
||||
const { path } = this.args;
|
||||
const { template, module, language } = this.flags;
|
||||
|
||||
const env = await createEnv(); // await needed because createEnv() uses dynamic import() for ESM-only yeoman-environment
|
||||
const ProjectGenerator = await ProjectGeneratorPromise; // await needed because generator classes are now created via ESM dynamic import
|
||||
env.registerStub(ProjectGenerator, 'zapier:integration');
|
||||
|
||||
await env.run('zapier:integration', { path, template, module, language });
|
||||
|
||||
this.log();
|
||||
this.log(`A new integration has been created in directory "${path}".`);
|
||||
this.log(`Read all about it in "${join(path, 'README.md')}".`);
|
||||
}
|
||||
}
|
||||
|
||||
InitCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
template: Flags.string({
|
||||
char: 't',
|
||||
description: 'The template to start your integration with.',
|
||||
options: TEMPLATE_CHOICES,
|
||||
}),
|
||||
module: Flags.string({
|
||||
char: 'm',
|
||||
description:
|
||||
'Choose module type: CommonJS or ES Modules. Only enabled for Typescript and Minimal templates.',
|
||||
options: ['commonjs', 'esm'],
|
||||
}),
|
||||
language: Flags.string({
|
||||
char: 'l',
|
||||
description:
|
||||
'Choose the language to use for your new integration. Defaults to JavaScript.',
|
||||
options: ['javascript', 'typescript'],
|
||||
}),
|
||||
},
|
||||
});
|
||||
InitCommand.args = {
|
||||
path: Args.string({
|
||||
description:
|
||||
"Where to create the new integration. If the directory doesn't exist, it will be created. If the directory isn't empty, we'll ask for confirmation",
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
InitCommand.examples = [
|
||||
'zapier-platform init myapp',
|
||||
'zapier-platform init ./path/myapp --template oauth2',
|
||||
'zapier-platform init ./path/myapp --template minimal --module esm',
|
||||
'zapier-platform init ./path/myapp --template oauth2 --language typescript',
|
||||
];
|
||||
InitCommand.description = `Initialize a new Zapier integration with a project template.
|
||||
|
||||
After running this, you'll have a new integration in the specified directory. If you re-run this command on an existing directory, it will prompt before overwriting any existing files.
|
||||
|
||||
This doesn't register or deploy the integration with Zapier - try the \`zapier-platform register\` and \`zapier-platform push\` commands for that!`;
|
||||
|
||||
InitCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = InitCommand;
|
||||
35
vendor/zapier-platform/packages/cli/src/oclif/commands/integrations.js
vendored
Normal file
35
vendor/zapier-platform/packages/cli/src/oclif/commands/integrations.js
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
|
||||
const { listApps } = require('../../utils/api');
|
||||
|
||||
class IntegrationsCommand extends BaseCommand {
|
||||
async perform() {
|
||||
this.startSpinner('Loading integrations');
|
||||
const { apps } = await listApps();
|
||||
this.stopSpinner();
|
||||
|
||||
this.log('\nHere are all the integrations you have write access to:');
|
||||
|
||||
this.logTable({
|
||||
rows: apps,
|
||||
headers: [
|
||||
['Title', 'title'],
|
||||
['Unique Slug', 'key'],
|
||||
['Date Created', 'date'],
|
||||
['Linked', 'linked'],
|
||||
],
|
||||
emptyMessage:
|
||||
'No integrations found, try the `zapier-platform register` command.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
IntegrationsCommand.flags = buildFlags({ opts: { format: true } });
|
||||
IntegrationsCommand.aliases = ['apps'];
|
||||
IntegrationsCommand.description = `List integrations you have admin access to.
|
||||
|
||||
This command also checks the current directory for a linked integration.`;
|
||||
IntegrationsCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = IntegrationsCommand;
|
||||
119
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/action.js
vendored
Normal file
119
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/action.js
vendored
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
const debug = require('debug')('zapier:invoke');
|
||||
const _ = require('lodash');
|
||||
|
||||
const { startSpinner, endSpinner } = require('../../../utils/display');
|
||||
const { customLogger } = require('./logger');
|
||||
const { localAppCommandWithRelayErrorHandler } = require('./relay');
|
||||
const { promptForFields } = require('./prompts');
|
||||
const resolveInputDataTypes = require('./input-types');
|
||||
const { fetchInputFields, remoteInvoke } = require('./remote');
|
||||
|
||||
/**
|
||||
* Invokes a trigger, create, or search action locally.
|
||||
* Handles the full flow: prompting for input fields, resolving types, and executing the perform method.
|
||||
* @param {import('../../ZapierBaseCommand')} command - The command instance for prompting
|
||||
* @param {Object} context - The execution context containing app definition, auth data, input data, etc.
|
||||
* @returns {Promise<*>} The action output
|
||||
*/
|
||||
const invokeAction = async (command, context) => {
|
||||
// Do these in order:
|
||||
// 1. Prompt for static input fields that alter dynamic fields
|
||||
// 2. {actionTypePlural}.{actionKey}.operation.inputFields
|
||||
// 3. Prompt for input fields again
|
||||
// 4. {actionTypePlural}.{actionKey}.operation.perform
|
||||
const action =
|
||||
context.appDefinition[context.actionTypePlural][context.actionKey];
|
||||
const staticInputFields = (action.operation.inputFields || []).filter(
|
||||
(f) => f.key,
|
||||
);
|
||||
debug('staticInputFields:', staticInputFields);
|
||||
|
||||
await promptForFields(command, context, staticInputFields, invokeAction);
|
||||
|
||||
let adverb;
|
||||
if (context.remote) {
|
||||
adverb = 'remotely';
|
||||
} else if (context.authId) {
|
||||
adverb = 'locally with relay';
|
||||
} else {
|
||||
adverb = 'locally';
|
||||
}
|
||||
|
||||
let methodName = `${context.actionTypePlural}.${action.key}.operation.inputFields`;
|
||||
startSpinner(`Invoking ${methodName} ${adverb}`);
|
||||
|
||||
let inputFields;
|
||||
if (context.remote) {
|
||||
inputFields = await fetchInputFields(context);
|
||||
} else {
|
||||
inputFields = await localAppCommandWithRelayErrorHandler({
|
||||
command: 'execute',
|
||||
method: methodName,
|
||||
bundle: {
|
||||
inputData: context.inputData,
|
||||
inputDataRaw: context.inputData, // At this point, inputData hasn't been transformed yet
|
||||
authData: context.authData,
|
||||
meta: context.meta,
|
||||
},
|
||||
zcacheTestObj: context.zcacheTestObj,
|
||||
cursorTestObj: context.cursorTestObj,
|
||||
customLogger,
|
||||
calledFromCliInvoke: true,
|
||||
appId: context.appId,
|
||||
deployKey: context.deployKey,
|
||||
relayAuthenticationId: context.authId,
|
||||
});
|
||||
}
|
||||
endSpinner();
|
||||
|
||||
debug('inputFields:', inputFields);
|
||||
|
||||
if (inputFields.length !== staticInputFields.length) {
|
||||
await promptForFields(command, context, inputFields, invokeAction);
|
||||
}
|
||||
|
||||
// Preserve original inputData as inputDataRaw before type resolution (deep
|
||||
// copy needed because resolveInputDataTypes mutates nested objects in-place)
|
||||
const inputDataRaw = _.cloneDeep(context.inputData);
|
||||
let inputData;
|
||||
if (context.remote) {
|
||||
// Let the remote server resolve input data types
|
||||
inputData = { ...context.inputData };
|
||||
} else {
|
||||
inputData = resolveInputDataTypes(
|
||||
context.inputData,
|
||||
inputFields,
|
||||
context.timezone,
|
||||
);
|
||||
}
|
||||
methodName = `${context.actionTypePlural}.${action.key}.operation.perform`;
|
||||
|
||||
startSpinner(`Invoking ${methodName} ${adverb}`);
|
||||
let output;
|
||||
if (context.remote) {
|
||||
output = await remoteInvoke(context);
|
||||
} else {
|
||||
output = await localAppCommandWithRelayErrorHandler({
|
||||
command: 'execute',
|
||||
method: methodName,
|
||||
bundle: {
|
||||
inputData,
|
||||
inputDataRaw,
|
||||
authData: context.authData,
|
||||
meta: context.meta,
|
||||
},
|
||||
zcacheTestObj: context.zcacheTestObj,
|
||||
cursorTestObj: context.cursorTestObj,
|
||||
customLogger,
|
||||
calledFromCliInvoke: true,
|
||||
appId: context.appId,
|
||||
deployKey: context.deployKey,
|
||||
relayAuthenticationId: context.authId,
|
||||
});
|
||||
}
|
||||
endSpinner();
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
module.exports = { invokeAction };
|
||||
6
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/auth/index.js
vendored
Normal file
6
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/auth/index.js
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
startAuth: require('./start').startAuth,
|
||||
testAuth: require('./test').testAuth,
|
||||
getAuthLabel: require('./label').getAuthLabel,
|
||||
refreshAuth: require('./refresh').refreshAuth,
|
||||
};
|
||||
22
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/auth/label.js
vendored
Normal file
22
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/auth/label.js
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const _ = require('lodash');
|
||||
|
||||
const { testAuth } = require('./test');
|
||||
|
||||
/**
|
||||
* Gets the connection label by running authentication.test and rendering the label template.
|
||||
* @param {Object} context - The execution context
|
||||
* @returns {Promise<string>} The rendered connection label
|
||||
*/
|
||||
const getAuthLabel = async (context) => {
|
||||
const testResult = await testAuth(context);
|
||||
const labelTemplate = (
|
||||
context.appDefinition.authentication.connectionLabel ?? ''
|
||||
).replaceAll('__', '.');
|
||||
const tpl = _.template(labelTemplate, { interpolate: /{{([\s\S]+?)}}/g });
|
||||
return tpl({
|
||||
...testResult,
|
||||
bundle: { authData: context.authData, inputData: testResult },
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { getAuthLabel };
|
||||
87
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/auth/refresh.js
vendored
Normal file
87
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/auth/refresh.js
vendored
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
const _ = require('lodash');
|
||||
|
||||
const { localAppCommand } = require('../../../../utils/local');
|
||||
const { startSpinner, endSpinner } = require('../../../../utils/display');
|
||||
const { customLogger } = require('../logger');
|
||||
|
||||
/**
|
||||
* Refreshes OAuth2 access token using the refresh token.
|
||||
* @param {Object} context - The execution context with current authData
|
||||
* @returns {Promise<Object>} New auth data with refreshed tokens
|
||||
*/
|
||||
const refreshOAuth2 = async (context) => {
|
||||
startSpinner('Invoking authentication.oauth2Config.refreshAccessToken');
|
||||
|
||||
const newAuthData = await localAppCommand({
|
||||
command: 'execute',
|
||||
method: 'authentication.oauth2Config.refreshAccessToken',
|
||||
bundle: {
|
||||
authData: context.authData,
|
||||
},
|
||||
zcacheTestObj: context.zcacheTestObj,
|
||||
customLogger,
|
||||
calledFromCliInvoke: true,
|
||||
});
|
||||
|
||||
endSpinner();
|
||||
return newAuthData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Refreshes session authentication by calling the session config perform method.
|
||||
* @param {Object} context - The execution context with current authData
|
||||
* @returns {Promise<Object>} New session data
|
||||
*/
|
||||
const refreshSessionAuth = async (context) => {
|
||||
startSpinner('Invoking authentication.sessionConfig.perform');
|
||||
|
||||
const sessionData = await localAppCommand({
|
||||
command: 'execute',
|
||||
method: 'authentication.sessionConfig.perform',
|
||||
bundle: {
|
||||
authData: context.authData,
|
||||
},
|
||||
zcacheTestObj: context.zcacheTestObj,
|
||||
customLogger,
|
||||
calledFromCliInvoke: true,
|
||||
});
|
||||
|
||||
endSpinner();
|
||||
return sessionData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Main entry point for refreshing authentication.
|
||||
* Routes to the appropriate refresh handler based on authentication type.
|
||||
* @param {Object} context - The execution context
|
||||
* @returns {Promise<Object|null>} New auth data or null if no authentication needed
|
||||
* @throws {Error} If auth type doesn't support refresh or no auth data exists
|
||||
*/
|
||||
const refreshAuth = async (context) => {
|
||||
const authentication = context.appDefinition.authentication;
|
||||
if (!authentication) {
|
||||
console.warn(
|
||||
"Your integration doesn't seem to need authentication. " +
|
||||
"If that isn't true, the app definition should have " +
|
||||
'an `authentication` object at the root level.',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (_.isEmpty(context.authData)) {
|
||||
throw new Error(
|
||||
'No auth data found in the .env file. Run `zapier-platform invoke auth start` first to initialize the auth data.',
|
||||
);
|
||||
}
|
||||
switch (authentication.type) {
|
||||
case 'oauth2':
|
||||
return refreshOAuth2(context);
|
||||
case 'session':
|
||||
return refreshSessionAuth(context);
|
||||
default:
|
||||
throw new Error(
|
||||
`This command doesn't support refreshing authentication type "${authentication.type}".`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { refreshAuth };
|
||||
31
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/auth/render.js
vendored
Normal file
31
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/auth/render.js
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
const { customLogger } = require('../logger');
|
||||
const { localAppCommand } = require('../../../../utils/local');
|
||||
|
||||
/**
|
||||
* Renders the auth template with real credentials from context.authData.
|
||||
* @param {Object} context - The execution context with authData
|
||||
* @returns {Promise<*>} The rendered auth template result
|
||||
*/
|
||||
const renderAuth = async (context) => {
|
||||
try {
|
||||
const result = await localAppCommand({
|
||||
command: 'renderAuthTemplate',
|
||||
bundle: {
|
||||
authData: context.authData,
|
||||
},
|
||||
customLogger,
|
||||
calledFromCliInvoke: true,
|
||||
});
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (err.message && err.message.includes('Unexpected command')) {
|
||||
throw new Error(
|
||||
'`auth render` requires latest version of zapier-platform-core. ' +
|
||||
'Upgrade zapier-platform-core in your dependencies.',
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { renderAuth };
|
||||
290
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/auth/start.js
vendored
Normal file
290
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/auth/start.js
vendored
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
const crypto = require('node:crypto');
|
||||
const http = require('node:http');
|
||||
|
||||
const _ = require('lodash');
|
||||
const debug = require('debug')('zapier:invoke');
|
||||
|
||||
const { localAppCommand } = require('../../../../utils/local');
|
||||
const { startSpinner, endSpinner } = require('../../../../utils/display');
|
||||
const { appendEnv } = require('../env');
|
||||
const { customLogger } = require('../logger');
|
||||
const { formatFieldDisplay } = require('../prompts');
|
||||
|
||||
/**
|
||||
* Prompts the user for authentication field values.
|
||||
* Handles password fields with hidden input.
|
||||
* @param {import('../../../ZapierBaseCommand')} command - The command instance for prompting
|
||||
* @param {Array<Object>} authFields - Array of auth field definitions
|
||||
* @returns {Promise<Object>} Object containing field keys and user-provided values
|
||||
*/
|
||||
const promptForAuthFields = async (command, authFields) => {
|
||||
const authData = {};
|
||||
for (const field of authFields) {
|
||||
if (field.computed) {
|
||||
continue;
|
||||
}
|
||||
const message = formatFieldDisplay(field) + ':';
|
||||
let value;
|
||||
if (field.type === 'password') {
|
||||
value = await command.promptHidden(message, true);
|
||||
} else {
|
||||
value = await command.prompt(message, { useStderr: true });
|
||||
}
|
||||
authData[field.key] = value;
|
||||
}
|
||||
return authData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes basic authentication by prompting for username and password.
|
||||
* @param {import('../../../ZapierBaseCommand')} command - The command instance for prompting
|
||||
* @param {Object} context - The execution context
|
||||
* @returns {Promise<Object>} Auth data with username and password
|
||||
* @throws {Error} If in non-interactive mode
|
||||
*/
|
||||
const startBasicAuth = async (command, context) => {
|
||||
if (context.nonInteractive) {
|
||||
throw new Error(
|
||||
'The `auth start` subcommand for "basic" authentication type only works in interactive mode.',
|
||||
);
|
||||
}
|
||||
return promptForAuthFields(command, [
|
||||
{
|
||||
key: 'username',
|
||||
label: 'Username',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: 'Password',
|
||||
required: true,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes custom authentication by prompting for configured auth fields.
|
||||
* @param {import('../../../ZapierBaseCommand')} command - The command instance for prompting
|
||||
* @param {Object} context - The execution context
|
||||
* @returns {Promise<Object>} Auth data with field values
|
||||
* @throws {Error} If in non-interactive mode
|
||||
*/
|
||||
const startCustomAuth = async (command, context) => {
|
||||
if (context.nonInteractive) {
|
||||
throw new Error(
|
||||
'The `auth start` subcommand for "custom" authentication type only works in interactive mode.',
|
||||
);
|
||||
}
|
||||
return promptForAuthFields(
|
||||
command,
|
||||
context.appDefinition.authentication.fields,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes OAuth2 authentication.
|
||||
* Prompts for CLIENT_ID/SECRET if needed, starts a local HTTP server,
|
||||
* opens the browser for authorization, and exchanges the code for tokens.
|
||||
* @param {import('../../../ZapierBaseCommand')} command - The command instance for prompting
|
||||
* @param {Object} context - The execution context
|
||||
* @returns {Promise<Object>} Auth data with access token and other OAuth2 fields
|
||||
*/
|
||||
const startOAuth2 = async (command, context) => {
|
||||
const env = {};
|
||||
|
||||
if (!process.env.CLIENT_ID || !process.env.CLIENT_SECRET) {
|
||||
if (context.nonInteractive) {
|
||||
throw new Error(
|
||||
'CLIENT_ID and CLIENT_SECRET must be set in the .env file in non-interactive mode.',
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
'CLIENT_ID and CLIENT_SECRET are required for OAuth2, ' +
|
||||
"but they are not found in the .env file. I'll prompt you for them now.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!process.env.CLIENT_ID) {
|
||||
env.CLIENT_ID = await command.prompt('CLIENT_ID:', { useStderr: true });
|
||||
process.env.CLIENT_ID = env.CLIENT_ID;
|
||||
}
|
||||
if (!process.env.CLIENT_SECRET) {
|
||||
env.CLIENT_SECRET = await command.prompt('CLIENT_SECRET:', {
|
||||
useStderr: true,
|
||||
});
|
||||
process.env.CLIENT_SECRET = env.CLIENT_SECRET;
|
||||
}
|
||||
|
||||
if (!_.isEmpty(env)) {
|
||||
// Save envs so the user won't have to re-enter them if the command fails
|
||||
await appendEnv(env);
|
||||
console.warn('CLIENT_ID and CLIENT_SECRET saved to .env file.');
|
||||
}
|
||||
|
||||
startSpinner('Invoking authentication.oauth2Config.authorizeUrl');
|
||||
|
||||
const stateParam = crypto.randomBytes(20).toString('hex');
|
||||
let authorizeUrl = await localAppCommand({
|
||||
command: 'execute',
|
||||
method: 'authentication.oauth2Config.authorizeUrl',
|
||||
bundle: {
|
||||
inputData: {
|
||||
response_type: 'code',
|
||||
redirect_uri: context.redirectUri,
|
||||
state: stateParam,
|
||||
},
|
||||
},
|
||||
zcacheTestObj: context.zcacheTestObj,
|
||||
customLogger,
|
||||
calledFromCliInvoke: true,
|
||||
});
|
||||
if (!authorizeUrl.includes('&scope=')) {
|
||||
const scope = context.appDefinition.authentication.oauth2Config.scope;
|
||||
if (scope) {
|
||||
authorizeUrl += `&scope=${encodeURIComponent(scope)}`;
|
||||
}
|
||||
}
|
||||
debug('authorizeUrl:', authorizeUrl);
|
||||
|
||||
endSpinner();
|
||||
startSpinner('Starting local HTTP server');
|
||||
|
||||
let resolveCode;
|
||||
const codePromise = new Promise((resolve) => {
|
||||
resolveCode = resolve;
|
||||
});
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
// Parse the request URL to extract the query parameters
|
||||
const code = new URL(req.url, context.redirectUri).searchParams.get('code');
|
||||
if (code) {
|
||||
resolveCode(code);
|
||||
debug(`Received code '${code}' from ${req.headers.referer}`);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end(
|
||||
'Parameter `code` received successfully. Go back to the terminal to continue.',
|
||||
);
|
||||
} else {
|
||||
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
||||
res.end(
|
||||
'Error: Did not receive `code` query parameter. ' +
|
||||
'Did you have the right CLIENT_ID and CLIENT_SECRET? ' +
|
||||
'Or did your server respond properly?',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await new Promise((resolve) => {
|
||||
server.listen(context.port, resolve);
|
||||
});
|
||||
|
||||
endSpinner();
|
||||
startSpinner('Opening browser to authorize (press Ctrl-C to exit on error)');
|
||||
|
||||
const { default: open } = await import('open');
|
||||
open(authorizeUrl);
|
||||
|
||||
const code = await codePromise;
|
||||
endSpinner();
|
||||
|
||||
startSpinner('Closing local HTTP server');
|
||||
await new Promise((resolve) => {
|
||||
server.close(resolve);
|
||||
});
|
||||
debug('Local HTTP server closed');
|
||||
|
||||
endSpinner();
|
||||
startSpinner('Invoking authentication.oauth2Config.getAccessToken');
|
||||
|
||||
const authData = await localAppCommand({
|
||||
command: 'execute',
|
||||
method: 'authentication.oauth2Config.getAccessToken',
|
||||
bundle: {
|
||||
authData: {},
|
||||
inputData: {
|
||||
code,
|
||||
redirect_uri: context.redirectUri,
|
||||
},
|
||||
},
|
||||
zcacheTestObj: context.zcacheTestObj,
|
||||
customLogger,
|
||||
calledFromCliInvoke: true,
|
||||
});
|
||||
|
||||
endSpinner();
|
||||
return authData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes session authentication.
|
||||
* Prompts for auth fields and then calls the session config perform method.
|
||||
* @param {import('../../../ZapierBaseCommand')} command - The command instance for prompting
|
||||
* @param {Object} context - The execution context
|
||||
* @returns {Promise<Object>} Combined auth data and session data
|
||||
* @throws {Error} If in non-interactive mode
|
||||
*/
|
||||
const startSessionAuth = async (command, context) => {
|
||||
if (context.nonInteractive) {
|
||||
throw new Error(
|
||||
'The `auth start` subcommand for "session" authentication type only works in interactive mode.',
|
||||
);
|
||||
}
|
||||
const authData = await promptForAuthFields(
|
||||
command,
|
||||
context.appDefinition.authentication.fields,
|
||||
);
|
||||
|
||||
startSpinner('Invoking authentication.sessionConfig.perform');
|
||||
const sessionData = await localAppCommand({
|
||||
command: 'execute',
|
||||
method: 'authentication.sessionConfig.perform',
|
||||
bundle: {
|
||||
authData,
|
||||
},
|
||||
zcacheTestObj: context.zcacheTestObj,
|
||||
customLogger,
|
||||
calledFromCliInvoke: true,
|
||||
});
|
||||
endSpinner();
|
||||
|
||||
return { ...authData, ...sessionData };
|
||||
};
|
||||
|
||||
/**
|
||||
* Main entry point for initializing authentication.
|
||||
* Routes to the appropriate auth type handler based on the app definition.
|
||||
* @param {import('../../../ZapierBaseCommand')} command - The command instance for prompting
|
||||
* @param {Object} context - The execution context
|
||||
* @returns {Promise<Object|null>} Auth data or null if no authentication needed
|
||||
* @throws {Error} If the authentication type is not supported
|
||||
*/
|
||||
const startAuth = async (command, context) => {
|
||||
const authentication = context.appDefinition.authentication;
|
||||
if (!authentication) {
|
||||
console.warn(
|
||||
"Your integration doesn't seem to need authentication. " +
|
||||
"If that isn't true, the app definition should have " +
|
||||
'an `authentication` object at the root level.',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
switch (authentication.type) {
|
||||
case 'basic':
|
||||
return startBasicAuth(command, context);
|
||||
case 'custom':
|
||||
return startCustomAuth(command, context);
|
||||
case 'oauth2':
|
||||
return startOAuth2(command, context);
|
||||
case 'session':
|
||||
return startSessionAuth(command, context);
|
||||
default:
|
||||
// TODO: Add support for 'digest' and 'oauth1'
|
||||
throw new Error(
|
||||
`This command doesn't support authentication type "${authentication.type}".`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { startAuth };
|
||||
30
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/auth/template.js
vendored
Normal file
30
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/auth/template.js
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
const { customLogger } = require('../logger');
|
||||
const { localAppCommand } = require('../../../../utils/local');
|
||||
|
||||
/**
|
||||
* Gets the auth template for the current app. No credentials needed.
|
||||
* Returns a template with {{bundle.authData.X}} placeholders.
|
||||
* @param {Object} context - The execution context
|
||||
* @returns {Promise<*>} The auth template result
|
||||
*/
|
||||
const templateAuth = async (context) => {
|
||||
try {
|
||||
const result = await localAppCommand({
|
||||
command: 'getAuthTemplate',
|
||||
bundle: {},
|
||||
customLogger,
|
||||
calledFromCliInvoke: true,
|
||||
});
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (err.message && err.message.includes('Unexpected command')) {
|
||||
throw new Error(
|
||||
'`auth template` requires latest version of zapier-platform-core. ' +
|
||||
'Upgrade zapier-platform-core in your dependencies.',
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { templateAuth };
|
||||
34
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/auth/test.js
vendored
Normal file
34
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/auth/test.js
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
const { startSpinner, endSpinner } = require('../../../../utils/display');
|
||||
const { customLogger } = require('../logger');
|
||||
const { localAppCommandWithRelayErrorHandler } = require('../relay');
|
||||
|
||||
/**
|
||||
* Tests authentication by invoking the authentication.test method.
|
||||
* Supports both local auth data and relay mode with production credentials.
|
||||
* @param {Object} context - The execution context with authData and optional authId
|
||||
* @returns {Promise<*>} The test result from the authentication.test method
|
||||
*/
|
||||
const testAuth = async (context) => {
|
||||
startSpinner('Invoking authentication.test');
|
||||
const result = await localAppCommandWithRelayErrorHandler({
|
||||
command: 'execute',
|
||||
method: 'authentication.test',
|
||||
bundle: {
|
||||
authData: context.authData,
|
||||
meta: {
|
||||
...context.meta,
|
||||
isTestingAuth: true,
|
||||
},
|
||||
},
|
||||
zcacheTestObj: context.zcacheTestObj,
|
||||
customLogger,
|
||||
calledFromCliInvoke: true,
|
||||
appId: context.appId,
|
||||
deployKey: context.deployKey,
|
||||
relayAuthenticationId: context.authId,
|
||||
});
|
||||
endSpinner();
|
||||
return result;
|
||||
};
|
||||
|
||||
module.exports = { testAuth };
|
||||
62
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/env.js
vendored
Normal file
62
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/env.js
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
const fsP = require('node:fs/promises');
|
||||
|
||||
/** Prefix used for auth data fields in the .env file */
|
||||
const AUTH_FIELD_ENV_PREFIX = 'authData_';
|
||||
|
||||
/**
|
||||
* Loads authData from environment variables.
|
||||
* Looks for variables prefixed with AUTH_FIELD_ENV_PREFIX and parses JSON values.
|
||||
* @returns {Object} An object containing auth field keys and their values
|
||||
*/
|
||||
const loadAuthDataFromEnv = () => {
|
||||
return Object.entries(process.env)
|
||||
.filter(([k, v]) => k.startsWith(AUTH_FIELD_ENV_PREFIX))
|
||||
.reduce((authData, [k, v]) => {
|
||||
const fieldKey = k.substr(AUTH_FIELD_ENV_PREFIX.length);
|
||||
// Try to parse as JSON if it looks like JSON, otherwise keep as string
|
||||
try {
|
||||
authData[fieldKey] =
|
||||
v.startsWith('{') || v.startsWith('[') ? JSON.parse(v) : v;
|
||||
} catch (e) {
|
||||
// If JSON parsing fails, keep as string
|
||||
authData[fieldKey] = v;
|
||||
}
|
||||
return authData;
|
||||
}, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Appends variables to the .env file.
|
||||
* Handles proper formatting and ensures newline separation.
|
||||
* @param {Object} vars - Key-value pairs to append
|
||||
* @param {string} [prefix=''] - Prefix to add to each variable name
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const appendEnv = async (vars, prefix = '') => {
|
||||
const envFile = '.env';
|
||||
let content = Object.entries(vars)
|
||||
.filter(([k, v]) => v !== undefined)
|
||||
.map(
|
||||
([k, v]) =>
|
||||
`${prefix}${k}='${typeof v === 'object' && v !== null ? JSON.stringify(v) : v || ''}'\n`,
|
||||
)
|
||||
.join('');
|
||||
|
||||
// Check if .env file exists and doesn't end with newline
|
||||
try {
|
||||
const existingContent = await fsP.readFile(envFile, 'utf8');
|
||||
if (existingContent.length > 0 && !existingContent.endsWith('\n')) {
|
||||
content = '\n' + content;
|
||||
}
|
||||
} catch (error) {
|
||||
// File doesn't exist or can't be read, proceed as normal
|
||||
}
|
||||
|
||||
await fsP.appendFile(envFile, content);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
AUTH_FIELD_ENV_PREFIX,
|
||||
loadAuthDataFromEnv,
|
||||
appendEnv,
|
||||
};
|
||||
566
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/index.js
vendored
Normal file
566
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/index.js
vendored
Normal file
|
|
@ -0,0 +1,566 @@
|
|||
const fsP = require('node:fs/promises');
|
||||
|
||||
const _ = require('lodash');
|
||||
const { Args, Flags } = require('@oclif/core');
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
const BaseCommand = require('../../ZapierBaseCommand');
|
||||
const { buildFlags } = require('../../buildFlags');
|
||||
const { localAppCommand } = require('../../../utils/local');
|
||||
const { readAppPackageJson } = require('../../../utils/misc');
|
||||
const { getLinkedAppConfig, readCredentials } = require('../../../utils/api');
|
||||
const { AUTH_KEY } = require('../../../constants');
|
||||
|
||||
const {
|
||||
AUTH_FIELD_ENV_PREFIX,
|
||||
loadAuthDataFromEnv,
|
||||
appendEnv,
|
||||
} = require('./env');
|
||||
const { startAuth, testAuth, getAuthLabel, refreshAuth } = require('./auth');
|
||||
const { templateAuth } = require('./auth/template');
|
||||
const { renderAuth } = require('./auth/render');
|
||||
const { invokeAction } = require('./action');
|
||||
const { promptForAuthentication } = require('./prompts');
|
||||
|
||||
const ACTION_TYPE_PLURALS = {
|
||||
trigger: 'triggers',
|
||||
search: 'searches',
|
||||
create: 'creates',
|
||||
};
|
||||
|
||||
const ACTION_TYPES = ['auth', ...Object.keys(ACTION_TYPE_PLURALS)];
|
||||
|
||||
/**
|
||||
* Reads all data from a readable stream and returns it as a string.
|
||||
* @param {import('stream').Readable} stream - The readable stream to consume
|
||||
* @returns {Promise<string>} The concatenated stream contents
|
||||
*/
|
||||
const readStream = async (stream) => {
|
||||
const chunks = [];
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
return chunks.join('');
|
||||
};
|
||||
|
||||
class InvokeCommand extends BaseCommand {
|
||||
/**
|
||||
* Main entry point for the invoke command. Handles auth operations (start, test, label, refresh)
|
||||
* and action invocations (trigger, create, search).
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async perform() {
|
||||
// Execution context that will be passed around
|
||||
const context = {
|
||||
// Data directly from command args and flags
|
||||
remote: this.flags.remote,
|
||||
version: this.flags.version || (await readAppPackageJson()).version,
|
||||
authId: this.flags['authentication-id'],
|
||||
nonInteractive: this.flags['non-interactive'] || !process.stdin.isTTY,
|
||||
actionType: this.args.actionType,
|
||||
actionKey: this.args.actionKey,
|
||||
timezone: this.flags.timezone,
|
||||
redirectUri: this.flags['redirect-uri'],
|
||||
port: this.flags['local-port'],
|
||||
meta: {
|
||||
isLoadingSample: this.flags.isLoadingSample,
|
||||
isFillingDynamicDropdown: this.flags.isFillingDynamicDropdown,
|
||||
isPopulatingDedupe: this.flags.isPopulatingDedupe,
|
||||
limit: this.flags.limit,
|
||||
page: this.flags.page,
|
||||
paging_token: this.flags['paging-token'],
|
||||
isTestingAuth: false, // legacy property
|
||||
},
|
||||
|
||||
// Data to be filled later
|
||||
actionTypePlural: null,
|
||||
appDefinition: null,
|
||||
authData: {},
|
||||
appId: null,
|
||||
deployKey: null,
|
||||
inputData: null,
|
||||
|
||||
// These will be used to patch z.cache() and z.cursor()
|
||||
zcacheTestObj: {},
|
||||
cursorTestObj: {},
|
||||
};
|
||||
|
||||
const dotenvResult = dotenv.config({ override: true, quiet: true });
|
||||
if (!context.authId && _.isEmpty(dotenvResult.parsed)) {
|
||||
console.warn(
|
||||
'The .env file does not exist or is empty. ' +
|
||||
'You may need to set some environment variables in there if your code uses process.env.',
|
||||
);
|
||||
}
|
||||
|
||||
if (context.remote && !context.version) {
|
||||
throw new Error(
|
||||
'Cannot determine the version to invoke. ' +
|
||||
'Specify `--version` or make sure your package.json has a `version` field.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!context.actionType) {
|
||||
if (context.nonInteractive) {
|
||||
throw new Error(
|
||||
'You must specify ACTIONTYPE and ACTIONKEY in non-interactive mode.',
|
||||
);
|
||||
}
|
||||
context.actionType = await this.promptWithList(
|
||||
'Which action type would you like to invoke?',
|
||||
ACTION_TYPES,
|
||||
{ useStderr: true },
|
||||
);
|
||||
}
|
||||
|
||||
context.actionTypePlural = ACTION_TYPE_PLURALS[context.actionType];
|
||||
context.appDefinition = await localAppCommand({ command: 'definition' });
|
||||
|
||||
if (!context.actionKey) {
|
||||
if (context.nonInteractive) {
|
||||
throw new Error('You must specify ACTIONKEY in non-interactive mode.');
|
||||
}
|
||||
if (context.actionType === 'auth') {
|
||||
const actionKeys = [
|
||||
'label',
|
||||
'refresh',
|
||||
'render',
|
||||
'start',
|
||||
'template',
|
||||
'test',
|
||||
];
|
||||
context.actionKey = await this.promptWithList(
|
||||
'Which auth operation would you like to invoke?',
|
||||
actionKeys,
|
||||
{ useStderr: true },
|
||||
);
|
||||
} else {
|
||||
const actionKeys = Object.keys(
|
||||
context.appDefinition[context.actionTypePlural] || {},
|
||||
).sort();
|
||||
if (!actionKeys.length) {
|
||||
throw new Error(
|
||||
`No "${context.actionTypePlural}" found in your integration.`,
|
||||
);
|
||||
}
|
||||
|
||||
context.actionKey = await this.promptWithList(
|
||||
`Which "${context.actionType}" key would you like to invoke?`,
|
||||
actionKeys,
|
||||
{ useStderr: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
context.appId = (await getLinkedAppConfig(null, false))?.id;
|
||||
context.deployKey = (await readCredentials(false))[AUTH_KEY];
|
||||
|
||||
const hasAuth = Boolean(context.appDefinition.authentication);
|
||||
|
||||
if (
|
||||
context.authId === '-' ||
|
||||
context.authId === '' ||
|
||||
(context.remote && !context.authId && hasAuth)
|
||||
) {
|
||||
if (context.nonInteractive) {
|
||||
throw new Error(
|
||||
'You must specify an `--authentication-id` (an integer) in non-interactive mode.',
|
||||
);
|
||||
}
|
||||
context.authId = (await promptForAuthentication(this)).toString();
|
||||
}
|
||||
|
||||
if (context.remote && !context.authId && !hasAuth) {
|
||||
// The remote invoke API requires authentication_id in the POST body,
|
||||
// but the server accepts 0 for apps without authentication configured.
|
||||
context.authId = '0';
|
||||
}
|
||||
|
||||
if (context.authId) {
|
||||
context.authId = parseInt(context.authId);
|
||||
if (isNaN(context.authId)) {
|
||||
throw new Error(
|
||||
"`--authentication-id` must be an integer or '-' to select from available authentications.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Reject unsupported flags early for template/render commands
|
||||
if (
|
||||
context.actionType === 'auth' &&
|
||||
(context.actionKey === 'template' || context.actionKey === 'render') &&
|
||||
(context.remote || context.authId)
|
||||
) {
|
||||
throw new Error(
|
||||
`The \`--remote\` and \`--authentication-id\` flags are not applicable to \`auth ${context.actionKey}\`. ` +
|
||||
'This command runs locally using auth data from the .env file.',
|
||||
);
|
||||
}
|
||||
|
||||
if (context.authId && !context.remote) {
|
||||
// Fill authData with curlies if we're in relay mode
|
||||
const authFields = context.appDefinition.authentication.fields || [];
|
||||
for (const field of authFields) {
|
||||
if (field.key) {
|
||||
context.authData[field.key] = `{{${field.key}}}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load from .env as well even in relay mode, in case the integration code
|
||||
// assumes there are values in bundle.authData. Loading from .env at least
|
||||
// gives the developer an option to override the values in bundle.authData.
|
||||
context.authData = { ...context.authData, ...loadAuthDataFromEnv() };
|
||||
|
||||
// `auth render` accepts a positional JSON-encoded authData arg whose
|
||||
// values take precedence over .env. Useful for one-off rendering
|
||||
// without touching the .env file.
|
||||
if (this.args.authData) {
|
||||
if (context.actionType !== 'auth' || context.actionKey !== 'render') {
|
||||
throw new Error(
|
||||
'The authData positional argument is only supported by `auth render`.',
|
||||
);
|
||||
}
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(this.args.authData);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to parse authData as JSON: ${err.message}`);
|
||||
}
|
||||
if (
|
||||
parsed === null ||
|
||||
typeof parsed !== 'object' ||
|
||||
Array.isArray(parsed)
|
||||
) {
|
||||
throw new Error('authData must be a JSON object.');
|
||||
}
|
||||
context.authData = { ...context.authData, ...parsed };
|
||||
}
|
||||
|
||||
if (context.actionType === 'auth') {
|
||||
switch (context.actionKey) {
|
||||
case 'start': {
|
||||
if (context.authId) {
|
||||
throw new Error(
|
||||
'The `--authentication-id` flag is not applicable. ' +
|
||||
'The `auth start` subcommand is to initialize local auth data in the .env file, ' +
|
||||
'whereas `--authentication-id` is for proxying requests using production auth data.',
|
||||
);
|
||||
}
|
||||
const newAuthData = await startAuth(this, context);
|
||||
if (_.isEmpty(newAuthData)) {
|
||||
return;
|
||||
}
|
||||
await appendEnv(newAuthData, AUTH_FIELD_ENV_PREFIX);
|
||||
console.warn(
|
||||
'Auth data appended to .env file. Run `zapier-platform invoke auth test` to test it.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
case 'refresh': {
|
||||
if (context.authId) {
|
||||
throw new Error(
|
||||
'The `--authentication-id` flag is not applicable. ' +
|
||||
'The `auth refresh` subcommand can only refresh your local auth data in the .env file. ' +
|
||||
'You might want to run `auth test` instead, which tests and may refresh auth data with the specified authentication ID in production.',
|
||||
);
|
||||
}
|
||||
const newAuthData = await refreshAuth(context);
|
||||
if (_.isEmpty(newAuthData)) {
|
||||
return;
|
||||
}
|
||||
await appendEnv(newAuthData, AUTH_FIELD_ENV_PREFIX);
|
||||
console.warn(
|
||||
'Auth data has been refreshed and appended to .env file. Run `zapier-platform invoke auth test` to test it.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
case 'test': {
|
||||
const output = await testAuth(context);
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
return;
|
||||
}
|
||||
case 'label': {
|
||||
const labelTemplate =
|
||||
context.appDefinition.authentication.connectionLabel;
|
||||
if (labelTemplate && labelTemplate.startsWith('$func$')) {
|
||||
console.warn(
|
||||
'Function-based connection label is not supported yet. Printing auth test result instead.',
|
||||
);
|
||||
const output = await testAuth(context);
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
} else {
|
||||
const output = await getAuthLabel(context);
|
||||
if (output) {
|
||||
console.log(output);
|
||||
} else {
|
||||
console.warn('Connection label is empty.');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'template': {
|
||||
const output = await templateAuth(context);
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
return;
|
||||
}
|
||||
case 'render': {
|
||||
const output = await renderAuth(context);
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
return;
|
||||
}
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown auth operation "${context.actionKey}". ` +
|
||||
'The options are "label", "refresh", "render", "start", "template", and "test". \n',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const action =
|
||||
context.appDefinition[context.actionTypePlural][context.actionKey];
|
||||
if (!action) {
|
||||
throw new Error(
|
||||
`No "${context.actionType}" found with key "${context.actionKey}".`,
|
||||
);
|
||||
}
|
||||
|
||||
let { inputData } = this.flags;
|
||||
if (inputData) {
|
||||
if (inputData.startsWith('@')) {
|
||||
const filePath = inputData.substr(1);
|
||||
let inputStream;
|
||||
if (filePath === '-') {
|
||||
inputStream = process.stdin;
|
||||
} else {
|
||||
const fd = await fsP.open(filePath);
|
||||
inputStream = fd.createReadStream({ encoding: 'utf8' });
|
||||
}
|
||||
inputData = await readStream(inputStream);
|
||||
}
|
||||
context.inputData = JSON.parse(inputData);
|
||||
} else {
|
||||
context.inputData = {};
|
||||
}
|
||||
|
||||
const output = await invokeAction(this, context);
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InvokeCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
inputData: Flags.string({
|
||||
char: 'i',
|
||||
description:
|
||||
'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 @-.',
|
||||
}),
|
||||
isFillingDynamicDropdown: Flags.boolean({
|
||||
description:
|
||||
'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.',
|
||||
default: false,
|
||||
}),
|
||||
isLoadingSample: Flags.boolean({
|
||||
description:
|
||||
'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.',
|
||||
default: false,
|
||||
}),
|
||||
isPopulatingDedupe: Flags.boolean({
|
||||
description:
|
||||
'Set bundle.meta.isPopulatingDedupe to true. Only makes sense for a polling trigger. When true in production, the results of this poll will be used initialize the deduplication list rather than trigger a Zap. This happens when a user enables a Zap.',
|
||||
default: false,
|
||||
}),
|
||||
limit: Flags.integer({
|
||||
description:
|
||||
'Set bundle.meta.limit. Only makes sense for a trigger. When used in production, this indicates the number of items you should fetch. -1 means no limit.',
|
||||
default: -1,
|
||||
}),
|
||||
page: Flags.integer({
|
||||
char: 'p',
|
||||
description:
|
||||
'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.',
|
||||
default: 0,
|
||||
}),
|
||||
'non-interactive': Flags.boolean({
|
||||
description: 'Do not show interactive prompts.',
|
||||
default: false,
|
||||
}),
|
||||
timezone: Flags.string({
|
||||
char: 'z',
|
||||
description:
|
||||
'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.',
|
||||
default: 'America/Chicago',
|
||||
}),
|
||||
'redirect-uri': Flags.string({
|
||||
description:
|
||||
"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.",
|
||||
default: 'http://localhost:9000',
|
||||
}),
|
||||
'local-port': Flags.integer({
|
||||
description:
|
||||
'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.',
|
||||
default: 9000,
|
||||
}),
|
||||
remote: Flags.boolean({
|
||||
char: 'r',
|
||||
description:
|
||||
'Run your trigger/action remotely on Zapier production servers instead of locally. This requires deploying your integration first. Because this (remote) mode uses the same set of API endpoints as the Zap editor and other Zapier products, it allows you to verify exactly how your code will behave in production. Note that `--authentication-id` is required and implied in remote mode, as a production authentication is necessary to invoke in production.',
|
||||
default: false,
|
||||
}),
|
||||
version: Flags.string({
|
||||
char: 'v',
|
||||
description:
|
||||
'Only used when `--remote` is set. Specify a deployed version to invoke instead of the one currently set in your local package.json.',
|
||||
}),
|
||||
'authentication-id': Flags.string({
|
||||
char: 'a',
|
||||
description:
|
||||
'EXPERIMENTAL: Instead of using the local .env file, use the production authentication data with the given authentication ID (aka the "app connection" on Zapier). Find them at https://zapier.com/app/assets/connections (https://zpr.io/z8SjFTdnTFZ2 for instructions) or specify \'-\' to interactively select one from your available authentications. When specified, the code will still run locally, but all outgoing requests will be proxied through Zapier with the production auth data.',
|
||||
}),
|
||||
'paging-token': Flags.string({
|
||||
description:
|
||||
'Set bundle.meta.paging_token. Used for search pagination or bulk reads. When used in production, this indicates which page of items you should fetch.',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
InvokeCommand.args = {
|
||||
actionType: Args.string({
|
||||
description: 'The action type you want to invoke.',
|
||||
options: ACTION_TYPES,
|
||||
}),
|
||||
actionKey: Args.string({
|
||||
description:
|
||||
'The trigger/action key you want to invoke. If ACTIONTYPE is "auth", this can be "label", "refresh", "start", or "test".',
|
||||
}),
|
||||
authData: Args.string({
|
||||
description:
|
||||
'Only used by `auth render`. JSON-encoded object with auth field values (e.g. `\'{"access_token":"a_token"}\'`). Values here take precedence over the .env file.',
|
||||
// Don't auto-fill from piped stdin — that breaks `--inputData @-` usage
|
||||
// for non-auth actions, which pipes into stdin for inputData.
|
||||
ignoreStdin: true,
|
||||
}),
|
||||
};
|
||||
|
||||
InvokeCommand.examples = [
|
||||
'zapier-platform invoke',
|
||||
'zapier-platform invoke auth start',
|
||||
'zapier-platform invoke auth refresh',
|
||||
'zapier-platform invoke auth test',
|
||||
'zapier-platform invoke auth label',
|
||||
'zapier-platform invoke trigger new_recipe',
|
||||
`zapier-platform invoke create add_recipe --inputData '{"title": "Pancakes"}'`,
|
||||
'zapier-platform invoke search find_recipe -i @file.json --non-interactive',
|
||||
'cat file.json | zapier-platform invoke trigger new_recipe -i @-',
|
||||
'zapier-platform invoke search find_ticket --authentication-id 12345',
|
||||
'zapier-platform invoke create add_ticket -a -',
|
||||
'zapier-platform invoke trigger new_recipe --remote',
|
||||
'zapier-platform invoke trigger new_recipe -r -a 12345',
|
||||
'zapier-platform invoke -r -v 2.0.0 -a -',
|
||||
`zapier-platform invoke auth render '{"access_token":"a_token"}'`,
|
||||
];
|
||||
InvokeCommand.description = `Invoke an authentication method, a trigger, or a create/search action locally or remotely.
|
||||
|
||||
This command allows you to invoke your integration's authentication, triggers, and actions. With this tool, you can test and debug your integration code directly from your terminal without leaving your development environment and opening a browser.
|
||||
|
||||
Why use this command?
|
||||
|
||||
* Fast feedback loops: Verify your code changes instantly.
|
||||
* Step-by-step debugging: Use a debugger to step through your code locally.
|
||||
* Untruncated logs: View complete HTTP logs and errors in your terminal.
|
||||
|
||||
### Modes
|
||||
|
||||
The \`invoke\` command works in three modes:
|
||||
|
||||
1. Local mode (default): runs your code locally, and sends outgoing requests directly from your local machine.
|
||||
2. Relay mode (experimental): runs your code locally, but proxies all outgoing requests through Zapier using production authentication data.
|
||||
3. Remote mode: runs your code and sends outgoing requests entirely in/from Zapier production environment.
|
||||
|
||||
**Local mode** is the default mode. Without the \`--remote\` (or \`-r\`) flag or the \`--authentication-id\` (or \`-a\`) flag, the command runs in local mode. It's useful when you want to quickly test your integration code locally. You'll need to set up local auth data in the \`.env\` file using the \`zapier-platform invoke auth start\` command.
|
||||
|
||||
**Relay mode** is currently experimental. It's enabled when the \`-a\` flag is specified. It's useful when you want to test code locally but setting up local auth data is troublesome, such as when your OAuth2 server requires a non-localhost or HTTPS redirect URI. By specifying \`-a <authentication-id>\`, all outgoing requests will be proxied through Zapier's relay service using the production auth data with the given authentication ID. See the **Authentication** section below for more details.
|
||||
|
||||
Both local and relay mode **emulate** how your code would run in Zapier production environment, so the behavior might not be exactly the same. But we consider every inconsistency a bug or a limitation to be fixed. For 100% match with production behavior, use remote mode.
|
||||
|
||||
**Remote mode** is enabled when the \`--remote\` (or \`-r\`) flag is specified. It's useful when you want to verify how your code behaves in Zapier production environment. Note that remote mode requires deploying your integration first. If the \`-a\` flag is not specified, the command will prompt you to select one of your available authentications/connections in production. By default, the remote mode invokes the \`version\` set in your \`package.json\`. You can use the \`--version\` (or \`-v\`) flag to specify a different deployed version.
|
||||
|
||||
### Authentication
|
||||
|
||||
You can supply the authentcation data in two ways: Load from the local \`.env\` file or use the \`--authentication-id\` flag.
|
||||
|
||||
#### The local \`.env\` file
|
||||
|
||||
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-platform invoke auth start\` command to help you initialize it, or you can manually create it.
|
||||
|
||||
The \`zapier-platform 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.
|
||||
|
||||
Each line in the \`.env\` file should follow one of these formats:
|
||||
|
||||
* \`VAR_NAME=VALUE\` for environment variables
|
||||
* \`authData_FIELD_KEY=VALUE\` for auth data fields
|
||||
|
||||
For example, a \`.env\` file for an OAuth2 integration might look like this:
|
||||
|
||||
\`\`\`
|
||||
CLIENT_ID='your_client_id'
|
||||
CLIENT_SECRET='your_client_secret'
|
||||
authData_access_token='1234567890'
|
||||
authData_refresh_token='abcdefg'
|
||||
authData_account_name='zapier'
|
||||
\`\`\`
|
||||
|
||||
|
||||
#### The \`--authentication-id\` flag
|
||||
|
||||
Setting up local auth data can be troublesome. For instance, in OAuth2, you may have to configure your app server to allow localhost redirect URIs or use a port forwarding tool. This is sometimes not easy to get right.
|
||||
|
||||
The \`--authentication-id\` flag (\`-a\` for short) gives you an alternative (and perhaps easier) way to supply your auth data. You can use \`-a\` to specify an existing production authentication/connection. The available authentications can be found at https://zapier.com/app/assets/connections. Check https://zpr.io/z8SjFTdnTFZ2 for more instructions.
|
||||
|
||||
When \`-a -\` is specified, such as \`zapier-platform invoke auth test -a -\`, the command will interactively prompt you to select one of your available authentications.
|
||||
|
||||
If you know your authentication ID, you can specify it directly, such as \`zapier-platform invoke auth test -a 123456\`.
|
||||
|
||||
The \`-a\` flag also works in remote mode with the \`-r\` flag. In remote mode, if \`-a\` is not specified, such as \`zapier-platform invoke -r\`, the command will prompt you to select one of your available authentications.
|
||||
|
||||
#### Testing authentication
|
||||
|
||||
To test if the auth data is correct, run either one of these:
|
||||
|
||||
\`\`\`
|
||||
zapier-platform invoke auth test # invokes authentication.test method
|
||||
zapier-platform invoke auth label # invokes authentication.test and renders connection label
|
||||
\`\`\`
|
||||
|
||||
To refresh stale auth data for OAuth2 or session auth, run \`zapier-platform invoke auth refresh\`. Note that refreshing is only applicable for local auth data in the \`.env\` file.
|
||||
|
||||
### Invoking a trigger or an action
|
||||
|
||||
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\`:
|
||||
|
||||
\`\`\`
|
||||
zapier-platform invoke trigger new_recipe # (local mode)
|
||||
zapier-platform invoke trigger new_recipe -r # (remote mode)
|
||||
\`\`\`
|
||||
|
||||
To add input data, use the \`--inputData\` flag (\`-i\` for short). The input data can come from the command directly, a file, or stdin. See **EXAMPLES** below.
|
||||
|
||||
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.
|
||||
|
||||
The \`--debug\` flag will show you the HTTP request logs and any console logs you have in your code.
|
||||
|
||||
### Limitations in local and relay mode
|
||||
|
||||
The following is a non-exhaustive list of current limitations in local and relay mode. We may support them in the future.
|
||||
|
||||
- Hook triggers, including REST hook subscribe/unsubscribe
|
||||
- Output hydration
|
||||
- File upload
|
||||
- Function-based connection label
|
||||
- Buffered create actions
|
||||
- Search-or-create actions
|
||||
- Search-powered fields
|
||||
- autoRefresh for OAuth2 and session auth
|
||||
`;
|
||||
|
||||
module.exports = InvokeCommand;
|
||||
248
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/input-types.js
vendored
Normal file
248
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/input-types.js
vendored
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
const _ = require('lodash');
|
||||
|
||||
// Datetime related imports
|
||||
const chrono = require('chrono-node');
|
||||
const { DateTime, IANAZone } = require('luxon');
|
||||
|
||||
const FALSE_STRINGS = new Set([
|
||||
'noo',
|
||||
'no',
|
||||
'n',
|
||||
'false',
|
||||
'nope',
|
||||
'f',
|
||||
'never',
|
||||
'no thanks',
|
||||
'no thank you',
|
||||
'nul',
|
||||
'0',
|
||||
'none',
|
||||
'nil',
|
||||
'nill',
|
||||
'null',
|
||||
]);
|
||||
|
||||
const TRUE_STRINGS = new Set([['yes', 'yeah', 'y', 'true', 't', '1']]);
|
||||
|
||||
const NUMBER_CHARSET = '0123456789.-,';
|
||||
|
||||
/**
|
||||
* Parses a string value to a boolean like how Zapier production does.
|
||||
* Recognizes common truthy/falsy strings like 'yes', 'no', 'true', 'false', etc.
|
||||
* @param {string} s - The string to parse
|
||||
* @returns {boolean} The parsed boolean value
|
||||
*/
|
||||
const parseBoolean = (s) => {
|
||||
s = s.toLowerCase();
|
||||
if (TRUE_STRINGS.has(s)) {
|
||||
return true;
|
||||
}
|
||||
if (FALSE_STRINGS.has(s)) {
|
||||
return false;
|
||||
}
|
||||
return Boolean(s);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a string to a decimal number like how Zapier production does.
|
||||
* Extracts numeric characters and handles various number formats.
|
||||
* @param {string} s - The string to parse
|
||||
* @returns {number} The parsed decimal number
|
||||
*/
|
||||
const parseDecimal = (s) => {
|
||||
const chars = [];
|
||||
for (const c of s) {
|
||||
if (NUMBER_CHARSET.includes(c)) {
|
||||
chars.push(c);
|
||||
}
|
||||
}
|
||||
const cleaned = chars.join('').replace(/[.,-]$/, '');
|
||||
const result = parseFloat(cleaned);
|
||||
return isNaN(result) ? 0 : result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a string to an integer.
|
||||
* Falls back to parseDecimal if parseInt fails.
|
||||
* @param {string} s - The string to parse
|
||||
* @returns {number} The parsed integer
|
||||
*/
|
||||
const parseInteger = (s) => {
|
||||
const n = parseInt(s);
|
||||
if (!isNaN(n)) {
|
||||
return n;
|
||||
}
|
||||
return Math.floor(parseDecimal(s));
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a Unix timestamp string to an ISO datetime string.
|
||||
* Handles both seconds and milliseconds timestamps.
|
||||
* @param {string} dtString - String potentially containing a timestamp
|
||||
* @param {string} tzName - IANA timezone name
|
||||
* @returns {string|null} ISO datetime string or null if not a timestamp
|
||||
*/
|
||||
const parseTimestamp = (dtString, tzName) => {
|
||||
const match = dtString.match(/-?\d{10,14}/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
dtString = match[0];
|
||||
let timestamp = parseInt(dtString);
|
||||
if (dtString.length <= 12) {
|
||||
timestamp *= 1000;
|
||||
}
|
||||
|
||||
return DateTime.fromMillis(timestamp, { zone: tzName }).toFormat(
|
||||
"yyyy-MM-dd'T'HH:mm:ssZZ",
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if chrono parsing components contain time information.
|
||||
* @param {Object} parsingComps - Chrono parsing components
|
||||
* @returns {boolean} True if time info is present
|
||||
*/
|
||||
const hasTimeInfo = (parsingComps) => {
|
||||
const tags = [...parsingComps.tags()];
|
||||
for (const tag of tags) {
|
||||
if (tag.includes('ISOFormat') || tag.includes('Time')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds default time info (09:00:00) to parsing components if not present.
|
||||
* @param {Object} parsingComps - Chrono parsing components
|
||||
* @returns {Object} The modified parsing components
|
||||
*/
|
||||
const maybeImplyTimeInfo = (parsingComps) => {
|
||||
if (!hasTimeInfo(parsingComps)) {
|
||||
parsingComps.imply('hour', 9);
|
||||
parsingComps.imply('minute', 0);
|
||||
parsingComps.imply('second', 0);
|
||||
parsingComps.imply('millisecond', 0);
|
||||
}
|
||||
return parsingComps;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts chrono parsing components to an ISO datetime string (without timezone).
|
||||
* @param {Object} parsingComps - Chrono parsing components
|
||||
* @returns {string} ISO datetime string like "2024-01-15T09:00:00"
|
||||
*/
|
||||
const parsingCompsToString = (parsingComps) => {
|
||||
const yyyy = parsingComps.get('year');
|
||||
const mm = String(parsingComps.get('month')).padStart(2, '0');
|
||||
const dd = String(parsingComps.get('day')).padStart(2, '0');
|
||||
const hh = String(parsingComps.get('hour')).padStart(2, '0');
|
||||
const ii = String(parsingComps.get('minute')).padStart(2, '0');
|
||||
const ss = String(parsingComps.get('second')).padStart(2, '0');
|
||||
return `${yyyy}-${mm}-${dd}T${hh}:${ii}:${ss}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a datetime string using chrono-node with timezone support.
|
||||
* Handles timestamps, natural language dates, and ISO formats.
|
||||
* @param {string} dtString - The datetime string to parse
|
||||
* @param {string} tzName - IANA timezone name
|
||||
* @param {Date} now - Reference date for relative parsing
|
||||
* @returns {string} ISO datetime string with timezone offset
|
||||
*/
|
||||
const parseDatetime = (dtString, tzName, now) => {
|
||||
const timestampResult = parseTimestamp(dtString, tzName);
|
||||
if (timestampResult) {
|
||||
return timestampResult;
|
||||
}
|
||||
|
||||
const offset = IANAZone.create(tzName).offset(now.getTime());
|
||||
const results = chrono.parse(dtString, {
|
||||
instant: now,
|
||||
timezone: offset,
|
||||
});
|
||||
|
||||
let isoString;
|
||||
if (results.length) {
|
||||
const parsingComps = results[0].start;
|
||||
if (parsingComps.get('timezoneOffset') == null) {
|
||||
// No timezone info in the input string => interpret the datetime string
|
||||
// exactly as it is and append the timezone
|
||||
isoString = parsingCompsToString(maybeImplyTimeInfo(parsingComps));
|
||||
} else {
|
||||
// Timezone info is present or implied in the input string => convert the
|
||||
// datetime to the specified timezone
|
||||
isoString = maybeImplyTimeInfo(parsingComps).date().toISOString();
|
||||
}
|
||||
} else {
|
||||
// No datetime info in the input string => just return the current time
|
||||
isoString = now.toISOString();
|
||||
}
|
||||
|
||||
return DateTime.fromISO(isoString, { zone: tzName }).toFormat(
|
||||
"yyyy-MM-dd'T'HH:mm:ssZZ",
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves input data types based on field definitions.
|
||||
* Converts string values to appropriate types (integer, number, boolean, datetime).
|
||||
* Also applies default values for fields that have them.
|
||||
* @param {Object} inputData - The input data object (will be mutated)
|
||||
* @param {Array<Object>} inputFields - Array of field definitions with type info
|
||||
* @param {string} timezone - IANA timezone name for datetime parsing
|
||||
* @returns {Object} The mutated inputData object with resolved types
|
||||
*/
|
||||
const resolveInputDataTypes = (inputData, inputFields, timezone) => {
|
||||
const fieldsWithDefault = inputFields.filter((f) => f.default);
|
||||
for (const f of fieldsWithDefault) {
|
||||
if (!inputData[f.key]) {
|
||||
inputData[f.key] = f.default;
|
||||
}
|
||||
}
|
||||
|
||||
const inputFieldsByKey = _.keyBy(inputFields, 'key');
|
||||
for (const [k, v] of Object.entries(inputData)) {
|
||||
const inputField = inputFieldsByKey[k];
|
||||
if (!inputField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (inputField.type) {
|
||||
case 'integer':
|
||||
inputData[k] = parseInteger(v);
|
||||
break;
|
||||
case 'number':
|
||||
inputData[k] = parseDecimal(v);
|
||||
break;
|
||||
case 'boolean':
|
||||
inputData[k] = parseBoolean(v);
|
||||
break;
|
||||
case 'datetime':
|
||||
inputData[k] = parseDatetime(v, timezone, new Date());
|
||||
break;
|
||||
case 'file':
|
||||
// TODO: How to handle a file field?
|
||||
break;
|
||||
// TODO: Handle 'list' and 'dict' types?
|
||||
default:
|
||||
// No need to do anything with 'string' type?
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle line items (fields with "children")
|
||||
for (const field of inputFields) {
|
||||
if (field.children && field.children.length && Array.isArray(inputData[field.key])) {
|
||||
for (const item of inputData[field.key]) {
|
||||
resolveInputDataTypes(item, field.children, timezone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return inputData;
|
||||
};
|
||||
|
||||
module.exports = resolveInputDataTypes;
|
||||
13
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/logger.js
vendored
Normal file
13
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/logger.js
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
const debug = require('debug')('zapier:invoke');
|
||||
|
||||
/**
|
||||
* Custom logger for localAppCommand that logs messages and data using the debug module.
|
||||
* @param {string} message - The log message
|
||||
* @param {*} data - Additional data to log
|
||||
*/
|
||||
const customLogger = (message, data) => {
|
||||
debug(message);
|
||||
debug(data);
|
||||
};
|
||||
|
||||
module.exports = { customLogger };
|
||||
773
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/prompts.js
vendored
Normal file
773
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/prompts.js
vendored
Normal file
|
|
@ -0,0 +1,773 @@
|
|||
const _ = require('lodash');
|
||||
|
||||
const { listAuthentications } = require('../../../utils/api');
|
||||
const { startSpinner, endSpinner } = require('../../../utils/display');
|
||||
const { fetchChoices } = require('./remote');
|
||||
const { localAppCommandWithRelayErrorHandler } = require('./relay');
|
||||
const { customLogger } = require('./logger');
|
||||
|
||||
/**
|
||||
* Formats a field definition for display in prompts.
|
||||
* @param {Object} field - The field definition
|
||||
* @param {string} field.key - The field key
|
||||
* @param {string} [field.label] - The field label
|
||||
* @param {string} [field.type] - The field type (defaults to 'string')
|
||||
* @param {boolean} [field.required] - Whether the field is required
|
||||
* @returns {string} Formatted string like "Label | key | type | required"
|
||||
*/
|
||||
const formatFieldDisplay = (field) => {
|
||||
const ftype = field.type || 'string';
|
||||
let result;
|
||||
if (field.label) {
|
||||
result = `${field.label} | ${field.key} | ${ftype}`;
|
||||
} else {
|
||||
result = `${field.key} | ${ftype}`;
|
||||
}
|
||||
if (field.required) {
|
||||
result += ' | required';
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts a display label from an object for use in dynamic dropdowns.
|
||||
* Tries common label keys like 'name', 'title', 'display', etc.
|
||||
* @param {Object} obj - The object to extract a label from
|
||||
* @param {string} [preferredKey] - Preferred key to check first (supports nested paths with __)
|
||||
* @param {string} [fallbackKey] - Fallback key to check last (supports nested paths with __)
|
||||
* @returns {string} The extracted label or empty string if not found
|
||||
*/
|
||||
const getLabelForDynamicDropdown = (obj, preferredKey, fallbackKey) => {
|
||||
const keys = [
|
||||
'name',
|
||||
'Name',
|
||||
'display',
|
||||
'Display',
|
||||
'title',
|
||||
'Title',
|
||||
'subject',
|
||||
'Subject',
|
||||
];
|
||||
if (preferredKey) {
|
||||
keys.unshift(preferredKey.split('__'));
|
||||
}
|
||||
if (fallbackKey) {
|
||||
keys.push(fallbackKey.split('__'));
|
||||
}
|
||||
for (const key of keys) {
|
||||
const label = _.get(obj, key);
|
||||
if (label) {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters input fields to find required fields that are missing values.
|
||||
* @param {Object} inputData - The current input data
|
||||
* @param {Array<Object>} inputFields - Array of field definitions
|
||||
* @returns {Array<Object>} Array of required fields that have no value or default
|
||||
*/
|
||||
const getMissingRequiredInputFields = (inputData, inputFields) => {
|
||||
return inputFields.filter(
|
||||
(f) =>
|
||||
f.required &&
|
||||
!f.default &&
|
||||
(inputData[f.key] == null || inputData[f.key] === ''),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds required child fields (line items) that are missing values in any row.
|
||||
* @param {Object} inputData - The current input data
|
||||
* @param {Array<Object>} inputFields - Array of field definitions
|
||||
* @returns {Array<Object>} Array of required child fields missing in at least one row
|
||||
*/
|
||||
const getMissingRequiredChildFields = (inputData, inputFields) => {
|
||||
const missing = [];
|
||||
for (const field of inputFields) {
|
||||
if (!field.children || !field.children.length) {
|
||||
continue;
|
||||
}
|
||||
const items = inputData[field.key];
|
||||
if (!Array.isArray(items)) {
|
||||
continue;
|
||||
}
|
||||
const requiredChildren = field.children.filter(
|
||||
(c) => c.required && c.type !== 'copy' && !c.default,
|
||||
);
|
||||
for (const item of items) {
|
||||
for (const c of requiredChildren) {
|
||||
if (item[c.key] == null || item[c.key] === '') {
|
||||
missing.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return missing;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches choices for a dynamic dropdown field.
|
||||
* @param {import('../../ZapierBaseCommand')} command - The command instance for prompting
|
||||
* @param {Object} context - The execution context
|
||||
* @param {Object} field - The field definition
|
||||
* @param {Function} invokeAction - Function to invoke actions (used for dynamic dropdowns)
|
||||
* @returns {Promise<Array<Object>>} Array of choices formatted as { name, value } objects
|
||||
*/
|
||||
const getDynamicDropdownChoices = async (
|
||||
command,
|
||||
context,
|
||||
field,
|
||||
invokeAction,
|
||||
) => {
|
||||
if (context.remote) {
|
||||
return (await fetchChoices(context, field.key)).map((c) => ({
|
||||
name: `${c.label} (${c.value})`,
|
||||
value: c.value,
|
||||
}));
|
||||
} else {
|
||||
const [triggerKey, idField, labelField] = field.dynamic.split('.');
|
||||
const trigger = context.appDefinition.triggers[triggerKey];
|
||||
if (!trigger) {
|
||||
throw new Error(
|
||||
`Cannot find trigger "${triggerKey}" for dynamic dropdown of field "${field.key}".`,
|
||||
);
|
||||
}
|
||||
const newContext = {
|
||||
...context,
|
||||
nonInteractive: true,
|
||||
actionType: 'trigger',
|
||||
actionKey: triggerKey,
|
||||
actionTypePlural: 'triggers',
|
||||
meta: {
|
||||
...context.meta,
|
||||
isFillingDynamicDropdown: true,
|
||||
},
|
||||
};
|
||||
return (await invokeAction(command, newContext)).map((c) => {
|
||||
const id = c[idField] ?? 'null';
|
||||
const label = getLabelForDynamicDropdown(c, labelField, idField);
|
||||
return {
|
||||
name: `${label} (${id})`,
|
||||
value: id,
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether a field uses perform-based dynamic choices (choices: { perform }).
|
||||
* @param {Object} field - The field definition
|
||||
* @returns {boolean} True if the field has perform-based choices
|
||||
*/
|
||||
const isPerformBasedChoices = (field) =>
|
||||
field.choices &&
|
||||
typeof field.choices === 'object' &&
|
||||
!Array.isArray(field.choices) &&
|
||||
field.choices.perform !== undefined;
|
||||
|
||||
/**
|
||||
* Fetches choices for a perform-based dynamic dropdown field.
|
||||
* @param {import('../../ZapierBaseCommand')} command - The command instance
|
||||
* @param {Object} context - The execution context
|
||||
* @param {Object} field - The field definition with choices.perform
|
||||
* @returns {Promise<{choices: Array<Object>, nextPagingToken: string|null}>}
|
||||
*/
|
||||
const getPerformBasedChoices = async (command, context, field) => {
|
||||
if (context.remote) {
|
||||
const choices = (await fetchChoices(context, field.key)).map((c) => ({
|
||||
name: `${c.label} (${c.value})`,
|
||||
value: c.value,
|
||||
}));
|
||||
return { choices, nextPagingToken: null };
|
||||
}
|
||||
|
||||
// Find the field's index in the action's inputFields array
|
||||
const action =
|
||||
context.appDefinition[context.actionTypePlural][context.actionKey];
|
||||
const allInputFields = action.operation.inputFields || [];
|
||||
const fieldIndex = allInputFields.findIndex(
|
||||
(f) => f.key === field.key && f.choices && f.choices.perform,
|
||||
);
|
||||
if (fieldIndex === -1) {
|
||||
throw new Error(
|
||||
`Cannot find perform-based choices for field "${field.key}" in ` +
|
||||
`${context.actionTypePlural}.${context.actionKey}.operation.inputFields.`,
|
||||
);
|
||||
}
|
||||
|
||||
const methodName = `${context.actionTypePlural}.${context.actionKey}.operation.inputFields.${fieldIndex}.choices.perform`;
|
||||
const displayName = `${context.actionTypePlural}.${context.actionKey}.operation.inputFields[${fieldIndex}].choices.perform`;
|
||||
const adverb = context.remote
|
||||
? 'remotely'
|
||||
: context.authId
|
||||
? 'locally with relay'
|
||||
: 'locally';
|
||||
startSpinner(`Invoking ${displayName} ${adverb}`);
|
||||
const result = await localAppCommandWithRelayErrorHandler({
|
||||
command: 'execute',
|
||||
method: methodName,
|
||||
bundle: {
|
||||
inputData: context.inputData,
|
||||
inputDataRaw: context.inputData,
|
||||
authData: context.authData,
|
||||
meta: {
|
||||
...context.meta,
|
||||
isFillingDynamicDropdown: true,
|
||||
},
|
||||
},
|
||||
zcacheTestObj: context.zcacheTestObj,
|
||||
cursorTestObj: context.cursorTestObj,
|
||||
customLogger,
|
||||
calledFromCliInvoke: true,
|
||||
appId: context.appId,
|
||||
deployKey: context.deployKey,
|
||||
relayAuthenticationId: context.authId,
|
||||
});
|
||||
endSpinner();
|
||||
|
||||
// The perform function returns { results: [{ id, label }, ...], paging_token }
|
||||
// or a plain array of { id, label } objects
|
||||
let results, nextPagingToken;
|
||||
if (Array.isArray(result)) {
|
||||
results = result;
|
||||
nextPagingToken = null;
|
||||
} else {
|
||||
results = result.results || [];
|
||||
nextPagingToken = result.paging_token || null;
|
||||
}
|
||||
|
||||
const choices = results.map((c) => ({
|
||||
name: `${c.label || c.id} (${c.id})`,
|
||||
value: String(c.id),
|
||||
}));
|
||||
|
||||
return { choices, nextPagingToken };
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalizes static choices into an array of { name, value } objects for
|
||||
* prompting.
|
||||
* @param {Array|string|Object} choices - The static choices definition
|
||||
* @return {Array<Object>} Array of choices formatted as { name, value }
|
||||
*/
|
||||
const getStaticChoices = (choices) => {
|
||||
if (Array.isArray(choices)) {
|
||||
// Can be an array of string or an array of { value, label }
|
||||
if (choices.length === 0) {
|
||||
return [];
|
||||
} else if (typeof choices[0] === 'string') {
|
||||
return choices.map((x) => ({ name: `${x} (${x})`, value: x }));
|
||||
} else {
|
||||
return choices.map((c) => ({
|
||||
name: `${c.label} (${c.value})`,
|
||||
value: c.value,
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
// If choices is not an array, then it must be an object of { value: label }
|
||||
return Object.entries(choices).map(([value, label]) => ({
|
||||
name: `${label} (${value})`,
|
||||
value,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets choices for a dropdown field, handling static, trigger-based dynamic,
|
||||
* and perform-based dynamic cases.
|
||||
* @param {import('../../ZapierBaseCommand')} command - The command instance for prompting
|
||||
* @param {Object} context - The execution context
|
||||
* @param {Object} field - The field definition
|
||||
* @param {Function} invokeAction - Function to invoke actions (used for trigger-based dynamic dropdowns)
|
||||
* @param {Object} [pagingState] - Pagination state for perform-based choices
|
||||
* @param {boolean} [pagingState.hasPreviousPage] - Whether there is a previous page
|
||||
* @returns {Promise<{choices: Array<Object>, nextPagingToken: string|null}>}
|
||||
*/
|
||||
const getStaticOrDynamicDropdownChoices = async (
|
||||
command,
|
||||
context,
|
||||
field,
|
||||
invokeAction,
|
||||
pagingState,
|
||||
) => {
|
||||
if (field.dynamic) {
|
||||
const choices = await getDynamicDropdownChoices(
|
||||
command,
|
||||
context,
|
||||
field,
|
||||
invokeAction,
|
||||
);
|
||||
const page = context.meta.page || 0;
|
||||
if (page) {
|
||||
choices.unshift({
|
||||
name: `>>> PREVIOUS PAGE <<<`,
|
||||
value: '__prev_page__',
|
||||
});
|
||||
}
|
||||
choices.push({
|
||||
name: `>>> NEXT PAGE <<<`,
|
||||
value: '__next_page__',
|
||||
});
|
||||
return { choices, nextPagingToken: null };
|
||||
} else if (isPerformBasedChoices(field)) {
|
||||
const { choices, nextPagingToken } = await getPerformBasedChoices(
|
||||
command,
|
||||
context,
|
||||
field,
|
||||
);
|
||||
if (pagingState && pagingState.hasPreviousPage) {
|
||||
choices.unshift({
|
||||
name: `>>> PREVIOUS PAGE <<<`,
|
||||
value: '__prev_page__',
|
||||
});
|
||||
}
|
||||
if (nextPagingToken) {
|
||||
choices.push({
|
||||
name: `>>> NEXT PAGE <<<`,
|
||||
value: '__next_page__',
|
||||
});
|
||||
}
|
||||
return { choices, nextPagingToken };
|
||||
} else {
|
||||
return { choices: getStaticChoices(field.choices), nextPagingToken: null };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Prompts the user for a single field value.
|
||||
* Handles dynamic dropdowns, boolean fields, and regular text input.
|
||||
* @param {import('../../ZapierBaseCommand')} command - The command instance for prompting
|
||||
* @param {Object} context - The execution context
|
||||
* @param {Object} field - The field definition
|
||||
* @param {Function} invokeAction - Function to invoke actions (used for dynamic dropdowns)
|
||||
* @returns {Promise<string>} The user-provided value
|
||||
*/
|
||||
const promptForField = async (command, context, field, invokeAction) => {
|
||||
const message = formatFieldDisplay(field) + ':';
|
||||
if (field.dynamic || field.choices) {
|
||||
const performBased = isPerformBasedChoices(field);
|
||||
let answer;
|
||||
|
||||
// Paging state for perform-based choices (token-based pagination)
|
||||
const pagingTokenStack = [];
|
||||
let currentPagingToken = null;
|
||||
let nextPagingToken = null;
|
||||
|
||||
while (
|
||||
answer == null ||
|
||||
answer === '' ||
|
||||
answer === '__next_page__' ||
|
||||
answer === '__prev_page__'
|
||||
) {
|
||||
if (performBased) {
|
||||
switch (answer) {
|
||||
case '__next_page__':
|
||||
pagingTokenStack.push(currentPagingToken);
|
||||
currentPagingToken = nextPagingToken;
|
||||
break;
|
||||
case '__prev_page__':
|
||||
currentPagingToken = pagingTokenStack.pop() || null;
|
||||
break;
|
||||
}
|
||||
context = {
|
||||
...context,
|
||||
meta: {
|
||||
...context.meta,
|
||||
paging_token: currentPagingToken,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
let page = 0;
|
||||
switch (answer) {
|
||||
case '__next_page__':
|
||||
page = (context.meta.page || 0) + 1;
|
||||
break;
|
||||
case '__prev_page__':
|
||||
page = Math.max((context.meta.page || 0) - 1, 0);
|
||||
break;
|
||||
}
|
||||
context = {
|
||||
...context,
|
||||
meta: {
|
||||
...context.meta,
|
||||
page,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const result = await getStaticOrDynamicDropdownChoices(
|
||||
command,
|
||||
context,
|
||||
field,
|
||||
invokeAction,
|
||||
{ hasPreviousPage: pagingTokenStack.length > 0 },
|
||||
);
|
||||
nextPagingToken = result.nextPagingToken;
|
||||
answer = await command.promptWithList(message, result.choices, {
|
||||
useStderr: true,
|
||||
});
|
||||
}
|
||||
return answer;
|
||||
} else if (field.type === 'boolean') {
|
||||
if (field.required) {
|
||||
const yes = await command.confirm(message, false, false, true);
|
||||
return yes ? 'yes' : 'no';
|
||||
} else {
|
||||
return await command.prompt(message + ' (yes/no)', { useStderr: true });
|
||||
}
|
||||
} else {
|
||||
return await command.prompt(message, { useStderr: true });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Prompts for missing required fields or throws an error in non-interactive mode.
|
||||
* @param {import('../../ZapierBaseCommand')} command - The command instance for prompting
|
||||
* @param {Object} context - The execution context (inputData will be mutated)
|
||||
* @param {Array<Object>} inputFields - Array of field definitions
|
||||
* @param {Function} invokeAction - Function to invoke actions (used for dynamic dropdowns)
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If in non-interactive mode and required fields are missing
|
||||
*/
|
||||
const promptOrErrorForRequiredInputFields = async (
|
||||
command,
|
||||
context,
|
||||
inputFields,
|
||||
invokeAction,
|
||||
) => {
|
||||
// Check top-level required fields
|
||||
const missingFields = getMissingRequiredInputFields(
|
||||
context.inputData,
|
||||
inputFields,
|
||||
);
|
||||
if (missingFields.length) {
|
||||
if (context.nonInteractive || context.meta.isFillingDynamicDropdown) {
|
||||
throw new Error(
|
||||
"You're in non-interactive mode, so you must at least specify these required fields with --inputData: \n" +
|
||||
missingFields.map((f) => '* ' + formatFieldDisplay(f)).join('\n'),
|
||||
);
|
||||
}
|
||||
for (const f of missingFields) {
|
||||
context.inputData[f.key] = await promptForField(
|
||||
command,
|
||||
context,
|
||||
f,
|
||||
invokeAction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check required child fields (line items) per row
|
||||
const missingChildFields = getMissingRequiredChildFields(
|
||||
context.inputData,
|
||||
inputFields,
|
||||
);
|
||||
if (missingChildFields.length) {
|
||||
if (context.nonInteractive || context.meta.isFillingDynamicDropdown) {
|
||||
throw new Error(
|
||||
"You're in non-interactive mode, so you must at least specify these required fields with --inputData: \n" +
|
||||
missingChildFields
|
||||
.map((f) => '* ' + formatFieldDisplay(f))
|
||||
.join('\n'),
|
||||
);
|
||||
}
|
||||
// Prompt per row for missing child fields
|
||||
for (const field of inputFields) {
|
||||
if (!field.children || !field.children.length) {
|
||||
continue;
|
||||
}
|
||||
const items = context.inputData[field.key];
|
||||
if (!Array.isArray(items)) {
|
||||
continue;
|
||||
}
|
||||
const requiredChildren = field.children.filter(
|
||||
(c) => c.required && c.type !== 'copy' && !c.default,
|
||||
);
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
for (const c of requiredChildren) {
|
||||
if (items[i][c.key] == null || items[i][c.key] === '') {
|
||||
const label = field.label || field.key;
|
||||
console.error(`\n${label} (row ${i + 1}):`);
|
||||
items[i][c.key] = await promptForField(
|
||||
command,
|
||||
context,
|
||||
c,
|
||||
invokeAction,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Allows the user to interactively edit input field values.
|
||||
* Displays a list of fields and lets the user select which to edit.
|
||||
* @param {import('../../ZapierBaseCommand')} command - The command instance for prompting
|
||||
* @param {Object} context - The execution context (inputData will be mutated)
|
||||
* @param {Array<Object>} inputFields - Array of field definitions
|
||||
* @param {Function} invokeAction - Function to invoke actions (used for dynamic dropdowns)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const promptForInputFieldEdit = async (
|
||||
command,
|
||||
context,
|
||||
inputFields,
|
||||
invokeAction,
|
||||
) => {
|
||||
inputFields = inputFields.filter((f) => f.key);
|
||||
if (!inputFields.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Let user select which field to fill/edit
|
||||
while (true) {
|
||||
let fieldChoices = inputFields.map((f) => {
|
||||
let name;
|
||||
if (f.label) {
|
||||
name = `${f.label} (${f.key})`;
|
||||
} else {
|
||||
name = f.key;
|
||||
}
|
||||
const currentValue = context.inputData[f.key];
|
||||
if (currentValue != null && currentValue !== '') {
|
||||
if (Array.isArray(currentValue)) {
|
||||
const MAX_LEN = 60;
|
||||
const csv = currentValue
|
||||
.map((item) => `{${Object.values(item).join(',')}}`)
|
||||
.join(', ');
|
||||
const count = `(${currentValue.length} ${currentValue.length === 1 ? 'item' : 'items'})`;
|
||||
if (csv.length <= MAX_LEN) {
|
||||
name += ` [${csv}] ${count}`;
|
||||
} else {
|
||||
name += ` [${csv.slice(0, MAX_LEN)}...] ${count}`;
|
||||
}
|
||||
} else {
|
||||
name += ` [current: "${currentValue}"]`;
|
||||
}
|
||||
} else if (f.default) {
|
||||
name += ` [default: "${f.default}"]`;
|
||||
}
|
||||
return {
|
||||
name,
|
||||
value: f.key,
|
||||
};
|
||||
});
|
||||
fieldChoices = [
|
||||
{
|
||||
name: '>>> DONE <<<',
|
||||
short: 'DONE',
|
||||
value: null,
|
||||
},
|
||||
...fieldChoices,
|
||||
];
|
||||
const fieldKey = await command.promptWithList(
|
||||
'Would you like to edit any of these input fields? Select "DONE" when you are all set.',
|
||||
fieldChoices,
|
||||
{ useStderr: true },
|
||||
);
|
||||
if (!fieldKey) {
|
||||
break;
|
||||
}
|
||||
|
||||
const field = inputFields.find((f) => f.key === fieldKey);
|
||||
if (field.children && field.children.length) {
|
||||
await promptForLineItemEdit(command, context, field, invokeAction);
|
||||
} else {
|
||||
context.inputData[fieldKey] = await promptForField(
|
||||
command,
|
||||
context,
|
||||
field,
|
||||
invokeAction,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Prompts the user to add a new line item row by prompting for each child field.
|
||||
* @param {import('../../ZapierBaseCommand')} command - The command instance
|
||||
* @param {Object} context - The execution context
|
||||
* @param {Object} field - The parent field definition with children
|
||||
* @param {Function} invokeAction - Function to invoke actions
|
||||
* @returns {Promise<Object>} The new row object
|
||||
*/
|
||||
const promptForNewLineItemRow = async (
|
||||
command,
|
||||
context,
|
||||
field,
|
||||
invokeAction,
|
||||
) => {
|
||||
const row = {};
|
||||
for (const child of field.children) {
|
||||
if (child.default) {
|
||||
row[child.key] = child.default;
|
||||
}
|
||||
if (child.required) {
|
||||
row[child.key] = await promptForField(
|
||||
command,
|
||||
context,
|
||||
child,
|
||||
invokeAction,
|
||||
);
|
||||
}
|
||||
}
|
||||
return row;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sub-menu for editing line item rows: add, edit, remove rows.
|
||||
* @param {import('../../ZapierBaseCommand')} command - The command instance
|
||||
* @param {Object} context - The execution context (inputData will be mutated)
|
||||
* @param {Object} field - The parent field definition with children
|
||||
* @param {Function} invokeAction - Function to invoke actions
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const promptForLineItemEdit = async (command, context, field, invokeAction) => {
|
||||
if (!Array.isArray(context.inputData[field.key])) {
|
||||
context.inputData[field.key] = [];
|
||||
}
|
||||
const items = context.inputData[field.key];
|
||||
const label = field.label || field.key;
|
||||
|
||||
while (true) {
|
||||
const choices = [
|
||||
{ name: '>>> BACK <<<', short: 'BACK', value: '__back__' },
|
||||
{ name: '>>> ADD ITEM <<<', value: '__add__' },
|
||||
];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const parts = Object.entries(items[i])
|
||||
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
|
||||
.join(', ');
|
||||
choices.push({
|
||||
name: parts ? `[${i}] {${parts}}` : `[${i}] Empty item - select to edit`,
|
||||
value: `__select_${i}`,
|
||||
});
|
||||
}
|
||||
|
||||
const action = await command.promptWithList(
|
||||
`${label} [${items.length} ${items.length === 1 ? 'item' : 'items'}]:`,
|
||||
choices,
|
||||
{ useStderr: true },
|
||||
);
|
||||
|
||||
if (action === '__back__') {
|
||||
break;
|
||||
} else if (action === '__add__') {
|
||||
const row = await promptForNewLineItemRow(
|
||||
command,
|
||||
context,
|
||||
field,
|
||||
invokeAction,
|
||||
);
|
||||
items.push(row);
|
||||
} else if (action.startsWith('__select_')) {
|
||||
const idx = parseInt(action.slice(9));
|
||||
// Sub-menu loop for editing/deleting the selected item
|
||||
while (true) {
|
||||
const editChoices = [
|
||||
{ name: '>>> BACK <<<', short: 'BACK', value: '__back__' },
|
||||
];
|
||||
for (const child of field.children) {
|
||||
const current = items[idx] && items[idx][child.key];
|
||||
let choiceName;
|
||||
if (child.label) {
|
||||
choiceName = `${child.label} (${child.key})`;
|
||||
} else {
|
||||
choiceName = child.key;
|
||||
}
|
||||
if (current != null) {
|
||||
choiceName += ` [current: "${current}"]`;
|
||||
}
|
||||
editChoices.push({ name: choiceName, value: child.key });
|
||||
}
|
||||
editChoices.push({
|
||||
name: '>>> DELETE ITEM <<<',
|
||||
value: '__delete__',
|
||||
});
|
||||
|
||||
const editAction = await command.promptWithList(
|
||||
'Edit or delete the item?',
|
||||
editChoices,
|
||||
{ useStderr: true },
|
||||
);
|
||||
|
||||
if (editAction === '__back__') {
|
||||
break;
|
||||
} else if (editAction === '__delete__') {
|
||||
items.splice(idx, 1);
|
||||
break;
|
||||
} else {
|
||||
const child = field.children.find((c) => c.key === editAction);
|
||||
items[idx][editAction] = await promptForField(
|
||||
command,
|
||||
context,
|
||||
child,
|
||||
invokeAction,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Main entry point for field prompting. Handles required fields and optional editing.
|
||||
* @param {import('../../ZapierBaseCommand')} command - The command instance for prompting
|
||||
* @param {Object} context - The execution context (inputData will be mutated)
|
||||
* @param {Array<Object>} inputFields - Array of field definitions
|
||||
* @param {Function} invokeAction - Function to invoke actions (used for dynamic dropdowns)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const promptForFields = async (command, context, inputFields, invokeAction) => {
|
||||
await promptOrErrorForRequiredInputFields(
|
||||
command,
|
||||
context,
|
||||
inputFields,
|
||||
invokeAction,
|
||||
);
|
||||
if (!context.nonInteractive && !context.meta.isFillingDynamicDropdown) {
|
||||
await promptForInputFieldEdit(command, context, inputFields, invokeAction);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Prompts the user to select an authentication/connection from their available authentications.
|
||||
* @param {import('../../ZapierBaseCommand')} command - The command instance for prompting
|
||||
* @returns {Promise<number>} The selected authentication ID
|
||||
* @throws {Error} If no authentications are found for the integration
|
||||
*/
|
||||
const promptForAuthentication = async (command) => {
|
||||
const auths = (await listAuthentications()).authentications;
|
||||
if (!auths || auths.length === 0) {
|
||||
throw new Error(
|
||||
'No authentications/connections found for your integration. ' +
|
||||
'Add a new connection at https://zapier.com/app/assets/connections ' +
|
||||
'or use local auth data by removing the `--authentication-id` flag.',
|
||||
);
|
||||
}
|
||||
const authChoices = auths.map((auth) => ({
|
||||
name: `${auth.title} | ${auth.app_version} | ID: ${auth.id}`,
|
||||
value: auth.id,
|
||||
}));
|
||||
return command.promptWithList(
|
||||
'Which authentication/connection would you like to use?',
|
||||
authChoices,
|
||||
{ useStderr: true },
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
formatFieldDisplay,
|
||||
getLabelForDynamicDropdown,
|
||||
getMissingRequiredInputFields,
|
||||
promptForAuthentication,
|
||||
promptForField,
|
||||
promptOrErrorForRequiredInputFields,
|
||||
promptForInputFieldEdit,
|
||||
promptForFields,
|
||||
};
|
||||
97
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/relay.js
vendored
Normal file
97
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/relay.js
vendored
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
const { localAppCommand } = require('../../../utils/local');
|
||||
|
||||
/**
|
||||
* Before-request middleware that replaces {{}} template syntax with safe placeholders.
|
||||
* This bypasses node-fetch's URL validation when variables are used in URLs.
|
||||
* @param {Object} request - The request object
|
||||
* @returns {Promise<Object>} The modified request object
|
||||
*/
|
||||
const replaceDoubleCurlies = async (request) => {
|
||||
// Use lcurly-fieldName-rcurly instead of {{fieldName}} to bypass node-fetch's
|
||||
// URL validation in case the variable is used in a URL.
|
||||
if (request.url) {
|
||||
request.url = request.url
|
||||
.replaceAll('{{', 'lcurly-')
|
||||
.replaceAll('}}', '-rcurly');
|
||||
}
|
||||
|
||||
// The authorization header may confuse zapier.com and it's relay's job to add
|
||||
// it, so we delete it here.
|
||||
delete request.headers.authorization;
|
||||
delete request.headers.Authorization;
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
/**
|
||||
* After-response middleware that restores {{}} template syntax from placeholders.
|
||||
* @param {Object} response - The response object
|
||||
* @returns {Promise<Object>} The modified response object
|
||||
*/
|
||||
const restoreDoubleCurlies = async (response) => {
|
||||
if (response.url) {
|
||||
response.url = response.url
|
||||
.replaceAll('lcurly-', '{{')
|
||||
.replaceAll('-rcurly', '}}');
|
||||
}
|
||||
if (response.request?.url) {
|
||||
response.request.url = response.request.url
|
||||
.replaceAll('lcurly-', '{{')
|
||||
.replaceAll('-rcurly', '}}');
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wraps localAppCommand with relay mode support and error handling.
|
||||
* When relayAuthenticationId is provided, adds middleware to handle template syntax
|
||||
* and provides better error messages for domain filter errors.
|
||||
* @param {Object} args - Arguments to pass to localAppCommand
|
||||
* @param {string} [args.relayAuthenticationId] - If provided, enables relay mode
|
||||
* @returns {Promise<*>} The command output
|
||||
* @throws {Error} Enhanced error for domain filter blocks
|
||||
*/
|
||||
const localAppCommandWithRelayErrorHandler = async (args) => {
|
||||
if (args.relayAuthenticationId) {
|
||||
args = {
|
||||
...args,
|
||||
beforeRequest: [replaceDoubleCurlies],
|
||||
afterResponse: [restoreDoubleCurlies],
|
||||
};
|
||||
}
|
||||
|
||||
let output;
|
||||
try {
|
||||
output = await localAppCommand(args);
|
||||
} catch (outerError) {
|
||||
if (outerError.name === 'ResponseError') {
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(outerError.message);
|
||||
} catch (innerError) {
|
||||
throw outerError;
|
||||
}
|
||||
if (typeof response.content === 'string') {
|
||||
const match = response.content.match(/domain filter `([^`]+)`/);
|
||||
if (!match) {
|
||||
throw outerError;
|
||||
}
|
||||
const domainFilter = match[1];
|
||||
const requestUrl = response.request.url
|
||||
.replaceAll('lcurly-', '{{')
|
||||
.replaceAll('-rcurly', '}}');
|
||||
throw new Error(
|
||||
`Request to ${requestUrl} was blocked. ` +
|
||||
`Only these domain names are allowed: ${domainFilter}. ` +
|
||||
'Contact Zapier team to verify your domain filter setting.',
|
||||
);
|
||||
}
|
||||
}
|
||||
throw outerError;
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
localAppCommandWithRelayErrorHandler,
|
||||
};
|
||||
167
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/remote.js
vendored
Normal file
167
vendor/zapier-platform/packages/cli/src/oclif/commands/invoke/remote.js
vendored
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
const { setTimeout: sleep } = require('node:timers/promises');
|
||||
|
||||
const { callAPI } = require('../../../utils/api');
|
||||
|
||||
const MAX_RESULT_POLLING_ATTEMPTS = 30;
|
||||
|
||||
const ACTION_TYPE_MAP = {
|
||||
trigger: 'read',
|
||||
create: 'write',
|
||||
search: 'search',
|
||||
};
|
||||
|
||||
const FIELD_TYPE_MAP = {
|
||||
password: 'password',
|
||||
unicode: 'string',
|
||||
text: 'text',
|
||||
int: 'integer',
|
||||
decimal: 'number',
|
||||
boolean: 'boolean',
|
||||
datetime: 'datetime',
|
||||
file: 'file',
|
||||
copy: 'copy',
|
||||
code: 'code',
|
||||
};
|
||||
|
||||
const fetchInputFields = async (context) => {
|
||||
const actionType = ACTION_TYPE_MAP[context.actionType];
|
||||
const responseData = await callAPI(
|
||||
`/apps/${context.appId}/versions/${context.version}/needs`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: {
|
||||
action_type: actionType,
|
||||
action_key: context.actionKey,
|
||||
authentication_id: context.authId,
|
||||
params: context.inputData,
|
||||
},
|
||||
},
|
||||
);
|
||||
const mapNeed = (need) => ({
|
||||
key: need.key,
|
||||
type: FIELD_TYPE_MAP[need.type] || need.type,
|
||||
required: need.required,
|
||||
default: need.default,
|
||||
choices: need.choices,
|
||||
label: need.label,
|
||||
helpText: need.help_text,
|
||||
inputFormat: need.input_format,
|
||||
dynamic: need.prefill,
|
||||
list: need.list,
|
||||
placeholder: need.placeholder,
|
||||
alterDynamicFields: need.alter_dynamic_fields ?? false,
|
||||
});
|
||||
|
||||
// Group child fields (those with parent_key) under their parent
|
||||
const parentKeys = new Set(
|
||||
responseData.needs.filter((n) => n.parent_key).map((n) => n.parent_key),
|
||||
);
|
||||
const fields = [];
|
||||
const childrenByParent = {};
|
||||
for (const need of responseData.needs) {
|
||||
if (need.parent_key) {
|
||||
if (!childrenByParent[need.parent_key]) {
|
||||
childrenByParent[need.parent_key] = [];
|
||||
}
|
||||
childrenByParent[need.parent_key].push(mapNeed(need));
|
||||
} else {
|
||||
fields.push(mapNeed(need));
|
||||
}
|
||||
}
|
||||
// Add parent fields for any parent_key that doesn't have a corresponding
|
||||
// top-level field in the response, then attach children
|
||||
for (const parentKey of parentKeys) {
|
||||
let parent = fields.find((f) => f.key === parentKey);
|
||||
if (!parent) {
|
||||
parent = { key: parentKey };
|
||||
fields.push(parent);
|
||||
}
|
||||
parent.children = childrenByParent[parentKey];
|
||||
}
|
||||
return fields;
|
||||
};
|
||||
|
||||
const fetchChoices = async (context, inputFieldKey) => {
|
||||
const actionType = ACTION_TYPE_MAP[context.actionType];
|
||||
const responseData = await callAPI(
|
||||
`/apps/${context.appId}/versions/${context.version}/choices`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: {
|
||||
action_type: actionType,
|
||||
action_key: context.actionKey,
|
||||
authentication_id: context.authId,
|
||||
params: context.inputData,
|
||||
input_field_key: inputFieldKey,
|
||||
page: String(context.meta.page || 0),
|
||||
},
|
||||
},
|
||||
);
|
||||
// Maybe we need to use responseData.next_page once we have function-based
|
||||
// dynamic choices?
|
||||
return responseData.choices.map((choice) => ({
|
||||
value: choice.key,
|
||||
label: choice.label,
|
||||
}));
|
||||
};
|
||||
|
||||
const formatInvokeResults = (context, results) => {
|
||||
if (context.actionType === 'create') {
|
||||
if (Array.isArray(results)) {
|
||||
if (results.length === 0) {
|
||||
return {};
|
||||
} else {
|
||||
// Remote invoke API always wraps a single object result in an array.
|
||||
// Unwrap it here to make it behave like the local invoke.
|
||||
return results[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
const pollForInvokeResult = async (context, invocationId) => {
|
||||
for (let i = 0; i < MAX_RESULT_POLLING_ATTEMPTS; i++) {
|
||||
const responseData = await callAPI(
|
||||
`/apps/${context.appId}/versions/${context.version}/invoke/${invocationId}`,
|
||||
);
|
||||
if (responseData.success) {
|
||||
switch (responseData.status) {
|
||||
case 'success':
|
||||
return formatInvokeResults(context, responseData.results);
|
||||
case 'error':
|
||||
throw new Error(responseData.errors.join('\n'));
|
||||
default:
|
||||
return responseData;
|
||||
}
|
||||
}
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Invocation timed out after ${MAX_RESULT_POLLING_ATTEMPTS} polling attempts.`,
|
||||
);
|
||||
};
|
||||
|
||||
const remoteInvoke = async (context) => {
|
||||
const actionType = ACTION_TYPE_MAP[context.actionType];
|
||||
const responseData = await callAPI(
|
||||
`/apps/${context.appId}/versions/${context.version}/invoke`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: {
|
||||
action_type: actionType,
|
||||
action_key: context.actionKey,
|
||||
authentication_id: context.authId,
|
||||
params: context.inputData,
|
||||
},
|
||||
},
|
||||
);
|
||||
return await pollForInvokeResult(context, responseData.invocation_id);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
fetchChoices,
|
||||
fetchInputFields,
|
||||
remoteInvoke,
|
||||
};
|
||||
100
vendor/zapier-platform/packages/cli/src/oclif/commands/jobs.js
vendored
Normal file
100
vendor/zapier-platform/packages/cli/src/oclif/commands/jobs.js
vendored
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
const { chain } = require('lodash');
|
||||
|
||||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const { listMigrations } = require('../../utils/api');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
|
||||
const getVersion = (versionStr) => versionStr.split('@')[1];
|
||||
const getIsoDate = (unixTs) => (unixTs ? new Date(unixTs).toISOString() : '-');
|
||||
|
||||
class JobsCommand extends BaseCommand {
|
||||
async perform() {
|
||||
/**
|
||||
* Migrations and Jobs are used somewhat interchangeably here.
|
||||
* Migrations represents the collection of promotion and migration
|
||||
* background "jobs" that were recently executed on this app.
|
||||
*/
|
||||
|
||||
this.startSpinner('Loading jobs');
|
||||
const { migrations } = await listMigrations();
|
||||
this.stopSpinner();
|
||||
|
||||
const jobs = chain(migrations)
|
||||
.filter(
|
||||
(migration) =>
|
||||
migration.job_kind === 'migrate' || migration.job_kind === 'promote',
|
||||
)
|
||||
.map((migration) => {
|
||||
const job = {
|
||||
app_title: migration.app_title,
|
||||
job_id: migration.job_id,
|
||||
job_kind: migration.job_kind,
|
||||
job_stage: migration.job_stage,
|
||||
version_from: getVersion(migration.from_selected_api),
|
||||
version_to: getVersion(migration.to_selected_api),
|
||||
started_at: getIsoDate(migration.started_at),
|
||||
updated_at: getIsoDate(migration.updated_at),
|
||||
};
|
||||
|
||||
if (migration.progress) {
|
||||
job.overall_progress =
|
||||
parseFloat(migration.progress.overall_progress * 100).toFixed(2) +
|
||||
'%';
|
||||
|
||||
if (migration.progress.current_step) {
|
||||
job.current_step = migration.progress.current_step.name;
|
||||
job.current_progress = `${migration.progress.current_step.finished_points} finished, ${migration.progress.current_step.skipped_points} skipped, ${migration.progress.current_step.estimated_points} estimated`;
|
||||
job.current_step_status = migration.progress.current_step.status;
|
||||
}
|
||||
}
|
||||
|
||||
if (migration.error) {
|
||||
job.error_message = migration.error.message;
|
||||
}
|
||||
|
||||
return job;
|
||||
})
|
||||
.sortBy((migration) => migration.started_at)
|
||||
.value();
|
||||
|
||||
this.logTable({
|
||||
rows: jobs,
|
||||
headers: [
|
||||
['App Title', 'app_title'],
|
||||
['Job Id', 'job_id'],
|
||||
['Job Kind', 'job_kind'],
|
||||
['Job Stage', 'job_stage'],
|
||||
['From', 'version_from'],
|
||||
['To', 'version_to'],
|
||||
['Current Step', 'current_step'],
|
||||
['Current Progress', 'current_progress'],
|
||||
['Current Step Status', 'current_step_status'],
|
||||
['Progress', 'overall_progress'],
|
||||
['Started At', 'started_at'],
|
||||
['Updated At', 'updated_at'],
|
||||
['Errors', 'error_message'],
|
||||
],
|
||||
emptyMessage:
|
||||
'No recent migration or promotion jobs found. Try `zapier-platform history` if you see older jobs.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
JobsCommand.examples = ['zapier-platform jobs'];
|
||||
JobsCommand.description = `Lists ongoing migration or promotion jobs for the current integration.
|
||||
|
||||
A job represents a background process that will be queued up when users execute a "migrate" or "promote" command for the current integration.
|
||||
|
||||
Each job will be added to the end of a queue of "promote" and "migration" jobs where the "Job Stage" will then be initialized with "requested".
|
||||
|
||||
Job stages will then move to "estimating", "in_progress" and finally one of four "end" stages: "complete", "aborted", "errored" or "paused".
|
||||
|
||||
Job times will vary as it depends on the size of the queue and how many users your integration has.
|
||||
|
||||
Jobs are returned from oldest to newest.
|
||||
`;
|
||||
|
||||
JobsCommand.flags = buildFlags({ opts: { format: true } });
|
||||
JobsCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = JobsCommand;
|
||||
61
vendor/zapier-platform/packages/cli/src/oclif/commands/legacy.js
vendored
Normal file
61
vendor/zapier-platform/packages/cli/src/oclif/commands/legacy.js
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const { Args, Flags } = require('@oclif/core');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
|
||||
const { callAPI } = require('../../utils/api');
|
||||
|
||||
class LegacyCommand extends BaseCommand {
|
||||
async perform() {
|
||||
const app = await this.getWritableApp();
|
||||
const { version } = this.args;
|
||||
|
||||
if (
|
||||
!this.flags.force &&
|
||||
!(await this.confirm(
|
||||
'Are you sure you want to mark this version as legacy? Existing Zaps and automations will continue to work, but users may not be able to create new Zaps or automations with this version.',
|
||||
))
|
||||
) {
|
||||
this.log('\nCancelled, version is not marked as legacy.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(
|
||||
`\nPreparing to mark version ${version} your app "${app.title}" as legacy.\n`,
|
||||
);
|
||||
const url = `/apps/${app.id}/versions/${version}/legacy`;
|
||||
this.startSpinner(`Making ${version} legacy`);
|
||||
await callAPI(url, {
|
||||
method: 'PUT',
|
||||
});
|
||||
this.stopSpinner();
|
||||
}
|
||||
}
|
||||
|
||||
LegacyCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
force: Flags.boolean({
|
||||
char: 'f',
|
||||
description: 'Skip confirmation prompt. Use with caution.',
|
||||
}),
|
||||
},
|
||||
});
|
||||
LegacyCommand.args = {
|
||||
version: Args.string({
|
||||
description: 'The version to mark as legacy.',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
LegacyCommand.examples = ['zapier-platform legacy 1.2.3'];
|
||||
LegacyCommand.description = `Mark a non-production version of your integration as legacy.
|
||||
|
||||
Use this when an integration version is no longer recommended for new users, but you don't want to block existing users from using it.
|
||||
|
||||
Reasons why you might want to mark a version as legacy:
|
||||
- this version may be discontinued in the future
|
||||
- this version has bugs
|
||||
- a newer version has been released and you want to encourage users to upgrade
|
||||
|
||||
`;
|
||||
LegacyCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = LegacyCommand;
|
||||
52
vendor/zapier-platform/packages/cli/src/oclif/commands/link.js
vendored
Normal file
52
vendor/zapier-platform/packages/cli/src/oclif/commands/link.js
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
const { cyan } = require('colors/safe');
|
||||
const { sortBy } = require('lodash');
|
||||
|
||||
const {
|
||||
listApps,
|
||||
writeLinkedAppConfig,
|
||||
getLinkedAppConfig,
|
||||
} = require('../../utils/api');
|
||||
const { CURRENT_APP_FILE } = require('../../constants');
|
||||
|
||||
class LinkCommand extends BaseCommand {
|
||||
async perform() {
|
||||
this.startSpinner('Loading integrations');
|
||||
const linkedAppId = (await getLinkedAppConfig(undefined, false)).id;
|
||||
const { apps } = await listApps();
|
||||
this.stopSpinner();
|
||||
|
||||
const chosenApp = await this.promptWithList(
|
||||
'Which integration should be associated with the code in this directory?',
|
||||
sortBy(
|
||||
apps.map((app) => ({
|
||||
name: `${app.title} (${app.id})${
|
||||
linkedAppId && app.id === linkedAppId
|
||||
? ' [currently linked app]'
|
||||
: ''
|
||||
}`,
|
||||
short: app.title,
|
||||
value: { id: app.id, key: app.key },
|
||||
})),
|
||||
(app) => app.name.toLowerCase(),
|
||||
),
|
||||
{ pageSize: 15 },
|
||||
);
|
||||
|
||||
this.startSpinner(`Setting up ${CURRENT_APP_FILE}`);
|
||||
await writeLinkedAppConfig(chosenApp);
|
||||
this.stopSpinner();
|
||||
this.log(`Done! Now you can \`${cyan('zapier-platform push')}\``);
|
||||
}
|
||||
}
|
||||
|
||||
LinkCommand.skipValidInstallCheck = true;
|
||||
LinkCommand.flags = buildFlags();
|
||||
LinkCommand.description = `Link the current directory with an existing integration.
|
||||
|
||||
This command generates a \`${CURRENT_APP_FILE}\` file in the directory in which it's ran. This file ties this code to an integration and is referenced frequently during \`push\` and \`validate\` operations. This file should be checked into source control.
|
||||
|
||||
If you're starting an integration from scratch, use \`zapier-platform init\` instead.`;
|
||||
|
||||
module.exports = LinkCommand;
|
||||
156
vendor/zapier-platform/packages/cli/src/oclif/commands/login.js
vendored
Normal file
156
vendor/zapier-platform/packages/cli/src/oclif/commands/login.js
vendored
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
const colors = require('colors/safe');
|
||||
|
||||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
const { Flags } = require('@oclif/core');
|
||||
|
||||
const {
|
||||
AUTH_LOCATION,
|
||||
AUTH_LOCATION_RAW,
|
||||
AUTH_KEY,
|
||||
BASE_ENDPOINT,
|
||||
} = require('../../constants');
|
||||
const {
|
||||
readCredentials,
|
||||
checkCredentials,
|
||||
createCredentials,
|
||||
} = require('../../utils/api');
|
||||
const { writeFile } = require('../../utils/files');
|
||||
const { prettyJSONstringify } = require('../../utils/display');
|
||||
const { isSamlEmail } = require('../../utils/credentials');
|
||||
|
||||
const getDeployKeyUrl = () => {
|
||||
const url = new URL(BASE_ENDPOINT);
|
||||
url.hostname = `developer.${url.hostname}`;
|
||||
url.pathname = 'partner-settings/deploy-keys/';
|
||||
return url.href;
|
||||
};
|
||||
const DEPLOY_KEY_DASH_URL = getDeployKeyUrl();
|
||||
|
||||
const isValidTotpCode = (i) => {
|
||||
const num = parseInt(i, 10);
|
||||
return Number.isInteger(num) && i.length === 6
|
||||
? true
|
||||
: 'Must be a 6 digit number';
|
||||
};
|
||||
const isValidDeployKey = (k) =>
|
||||
k.length === 32
|
||||
? true
|
||||
: `Must be a 32-character code copied from from ${DEPLOY_KEY_DASH_URL}`;
|
||||
|
||||
/**
|
||||
* there are a few says that someone might log into zapier:
|
||||
* 1. Username + Password
|
||||
* 2. Google/FB/etc SSO
|
||||
* 3. Company-configured SAML
|
||||
*
|
||||
* Group 1 will definitely have a password. Group 2 might have a password if they created one, but might not. Group 3 definitely will not.
|
||||
*/
|
||||
class LoginCommand extends BaseCommand {
|
||||
promptForDeployKey() {
|
||||
this.log(
|
||||
`To generate a deploy key, go to ${DEPLOY_KEY_DASH_URL} and create/copy a key, then paste the result below.`,
|
||||
);
|
||||
return this.prompt('Paste your Deploy Key here:', {
|
||||
validate: isValidDeployKey,
|
||||
});
|
||||
}
|
||||
|
||||
async perform() {
|
||||
const checks = [
|
||||
readCredentials()
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
checkCredentials()
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
];
|
||||
const [credentialsPresent, credentialsGood] = await Promise.all(checks);
|
||||
|
||||
if (!credentialsPresent) {
|
||||
this.stopSpinner(); // end the spinner in checkCredentials()
|
||||
this.log(
|
||||
colors.yellow(`Your ${AUTH_LOCATION} has not been set up yet.\n`),
|
||||
);
|
||||
} else if (!credentialsGood) {
|
||||
this.log(
|
||||
colors.red(
|
||||
`Your ${AUTH_LOCATION} looks like it has invalid credentials.\n`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
this.log(
|
||||
colors.green(
|
||||
`Your ${AUTH_LOCATION} looks valid. You may update it now though.\n`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let deployKey;
|
||||
|
||||
if (this.flags.sso) {
|
||||
// category 3
|
||||
deployKey = await this.promptForDeployKey();
|
||||
} else {
|
||||
const email = await this.prompt(
|
||||
'What email address do you use to log into Zapier?',
|
||||
);
|
||||
if (await isSamlEmail(email)) {
|
||||
// category 2
|
||||
deployKey = await this.promptForDeployKey();
|
||||
} else {
|
||||
// category 1
|
||||
this.log(
|
||||
`\n\nNow you'll enter your Zapier password.\nIf you log into Zapier via the ${colors.green(
|
||||
'log in with Google button',
|
||||
)} (or a different social network), you may not have a Zapier password.\nIf that's the case, hit CTRL+C and re-run this command with the ${colors.cyan(
|
||||
`--sso`,
|
||||
)} flag.\n\n`,
|
||||
);
|
||||
const password = await this.promptHidden(
|
||||
'What is your Zapier password?',
|
||||
);
|
||||
|
||||
let goodResponse;
|
||||
try {
|
||||
goodResponse = await createCredentials(email, password);
|
||||
} catch ({ errText, json: { errors } }) {
|
||||
if (errors[0].startsWith('missing totp_code')) {
|
||||
const code = await this.prompt(
|
||||
'What is your current 6-digit 2FA code?',
|
||||
{ validate: isValidTotpCode },
|
||||
);
|
||||
goodResponse = await createCredentials(email, password, code);
|
||||
} else {
|
||||
this.error(errText);
|
||||
}
|
||||
}
|
||||
deployKey = goodResponse.key;
|
||||
}
|
||||
}
|
||||
await writeFile(
|
||||
AUTH_LOCATION,
|
||||
prettyJSONstringify({
|
||||
[AUTH_KEY]: deployKey,
|
||||
}),
|
||||
);
|
||||
|
||||
await checkCredentials();
|
||||
|
||||
this.log(`Your deploy key has been saved to ${AUTH_LOCATION}. `);
|
||||
}
|
||||
}
|
||||
|
||||
LoginCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
sso: Flags.boolean({
|
||||
char: 's',
|
||||
description:
|
||||
"Use this flag if you log into Zapier a Single Sign-On (SSO) button and don't have a Zapier password.",
|
||||
}),
|
||||
},
|
||||
});
|
||||
LoginCommand.description = `Configure your \`${AUTH_LOCATION_RAW}\` with a deploy key.`;
|
||||
LoginCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = LoginCommand;
|
||||
37
vendor/zapier-platform/packages/cli/src/oclif/commands/logout.js
vendored
Normal file
37
vendor/zapier-platform/packages/cli/src/oclif/commands/logout.js
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
|
||||
const { callAPI } = require('../../utils/api');
|
||||
const { deleteFile } = require('../../utils/files');
|
||||
const { AUTH_LOCATION, AUTH_LOCATION_RAW } = require('../../constants');
|
||||
|
||||
class LogoutCommand extends BaseCommand {
|
||||
async perform() {
|
||||
let success = true;
|
||||
this.startSpinner('Deactivating local deploy key');
|
||||
try {
|
||||
await callAPI('/keys', { method: 'DELETE', body: { single: true } });
|
||||
} catch (e) {
|
||||
success = false;
|
||||
this.error(
|
||||
`Deletion API request failed. Is your ${AUTH_LOCATION} already empty or invalid? If so, feel free to ignore this error.`,
|
||||
);
|
||||
} finally {
|
||||
this.stopSpinner({ success });
|
||||
}
|
||||
|
||||
this.startSpinner(`Destroying \`${AUTH_LOCATION}\``);
|
||||
const deletedFileResult = deleteFile(AUTH_LOCATION);
|
||||
this.debug(`file deletion success?: ${deletedFileResult}`);
|
||||
this.stopSpinner();
|
||||
|
||||
this.log();
|
||||
this.log('The active deploy key was deactivated');
|
||||
}
|
||||
}
|
||||
|
||||
LogoutCommand.flags = buildFlags();
|
||||
LogoutCommand.description = `Deactivate your active deploy key and reset \`${AUTH_LOCATION_RAW}\`.`;
|
||||
LogoutCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = LogoutCommand;
|
||||
124
vendor/zapier-platform/packages/cli/src/oclif/commands/logs.js
vendored
Normal file
124
vendor/zapier-platform/packages/cli/src/oclif/commands/logs.js
vendored
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
const { Flags } = require('@oclif/core');
|
||||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
const { grey } = require('colors/safe');
|
||||
const { pick } = require('lodash');
|
||||
|
||||
const { listLogs } = require('../../utils/api');
|
||||
|
||||
// pulled out so we can pull these explicitly to send to the server
|
||||
const commandFlags = {
|
||||
version: Flags.string({
|
||||
char: 'v',
|
||||
description: 'Filter logs to the specified version.',
|
||||
}),
|
||||
status: Flags.string({
|
||||
char: 's',
|
||||
description: 'Filter logs to only see errors or successes',
|
||||
options: ['any', 'success', 'error'],
|
||||
default: 'any', // this doesn't really need to be a status
|
||||
}),
|
||||
type: Flags.string({
|
||||
char: 't',
|
||||
description: 'See logs of the specified type',
|
||||
options: ['console', 'bundle', 'http'],
|
||||
default: 'console',
|
||||
}),
|
||||
detailed: Flags.boolean({
|
||||
// no char since it conflicts with --debug
|
||||
description: 'See extra info, like request/response body and headers.',
|
||||
}),
|
||||
user: Flags.string({
|
||||
char: 'u',
|
||||
description: 'Only show logs for this user. Defaults to your account.',
|
||||
default: 'me',
|
||||
}),
|
||||
limit: Flags.integer({
|
||||
description:
|
||||
'Cap the number of logs returned. Max is 50 (also the default)',
|
||||
default: 50,
|
||||
}),
|
||||
};
|
||||
|
||||
class LogsCommand extends BaseCommand {
|
||||
async perform() {
|
||||
this.startSpinner('Loading logs');
|
||||
|
||||
const flags = pick(this.flags, Object.keys(commandFlags));
|
||||
const { logs } = await listLogs(flags);
|
||||
this.stopSpinner();
|
||||
const hasLogs = Boolean(logs.length);
|
||||
|
||||
let headers;
|
||||
if (this.flags.type === 'http') {
|
||||
headers = [
|
||||
['Status', 'response_status_code'],
|
||||
['Method', 'request_method'],
|
||||
['URL', 'request_url'],
|
||||
['Querystring', 'request_params'],
|
||||
['Version', 'app_cli_version'],
|
||||
['Step ID', 'step'],
|
||||
// ['ID', 'id'],
|
||||
['Timestamp', 'timestamp'],
|
||||
];
|
||||
|
||||
if (this.flags.detailed) {
|
||||
headers = headers.concat([
|
||||
['Request Headers', 'request_headers'],
|
||||
['Request Body', 'request_data'],
|
||||
['Response Headers', 'response_headers'],
|
||||
['Response Body', 'response_content'],
|
||||
]);
|
||||
}
|
||||
} else if (this.flags.type === 'bundle') {
|
||||
headers = [
|
||||
['Log', 'message'],
|
||||
['Input', 'input'],
|
||||
['Output', 'output'],
|
||||
['Version', 'app_cli_version'],
|
||||
// ['ID', 'id'],
|
||||
['Timestamp', 'timestamp'],
|
||||
];
|
||||
} else {
|
||||
headers = [
|
||||
['Log', 'full_message'],
|
||||
['Version', 'app_cli_version'],
|
||||
['Step', 'step'],
|
||||
// ['ID', 'id'],
|
||||
['Timestamp', 'timestamp'],
|
||||
];
|
||||
}
|
||||
|
||||
this.logTable({
|
||||
rows: logs.reverse(), // oldest logs first
|
||||
headers,
|
||||
emptyMessage:
|
||||
'No logs found. Try adding some `z.request()`, `z.console.log()` and doing a `zapier-platform push`!\n',
|
||||
});
|
||||
|
||||
if (hasLogs) {
|
||||
this.log(grey(' Most recent logs near the bottom.'));
|
||||
|
||||
if (this.flags.type === 'http' && !this.flags.detailed) {
|
||||
this.log(
|
||||
grey(
|
||||
' TIP: Use `zapier-platform logs --type=http --detailed` to include response information.',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogsCommand.skipValidInstallCheck = true;
|
||||
LogsCommand.flags = buildFlags({
|
||||
commandFlags,
|
||||
opts: { format: true },
|
||||
});
|
||||
LogsCommand.description = `Print recent logs.
|
||||
|
||||
Logs are created when your integration is run as part of a Zap. They come from explicit calls to \`z.console.log()\`, usage of \`z.request()\`, and any runtime errors.
|
||||
|
||||
This won't show logs from running locally with \`zapier-platform test\`, since those never hit our server.`;
|
||||
|
||||
module.exports = LogsCommand;
|
||||
220
vendor/zapier-platform/packages/cli/src/oclif/commands/migrate.js
vendored
Normal file
220
vendor/zapier-platform/packages/cli/src/oclif/commands/migrate.js
vendored
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
const _ = require('lodash');
|
||||
const debug = require('debug')('zapier:migrate');
|
||||
const { Args, Flags } = require('@oclif/core');
|
||||
const colors = require('colors/safe');
|
||||
|
||||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const PromoteCommand = require('./promote');
|
||||
const { callAPI } = require('../../utils/api');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
|
||||
class MigrateCommand extends BaseCommand {
|
||||
async run_require_confirmation_pre_checks(app, requestBody) {
|
||||
const assumeYes = 'yes' in this.flags;
|
||||
const url = `/apps/${app.id}/pre-migration-require-confirmation-checks`;
|
||||
|
||||
this.startSpinner(`Running pre-checks before migration...`);
|
||||
|
||||
try {
|
||||
await callAPI(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
body: requestBody,
|
||||
},
|
||||
true,
|
||||
);
|
||||
} catch (response) {
|
||||
this.stopSpinner({ success: false });
|
||||
|
||||
// 409 from the backend specifically signals pre-checks failed
|
||||
if (response.status === 409) {
|
||||
const softCheckErrors = _.get(response, 'json.errors', []);
|
||||
const formattedErrors = softCheckErrors.map((e) => `* ${e}`).join('\n');
|
||||
|
||||
this.log();
|
||||
this.log('Non-blocking checks prior to migration returned warnings:');
|
||||
this.log(formattedErrors);
|
||||
this.log();
|
||||
|
||||
const shouldContinuePreChecks =
|
||||
assumeYes ||
|
||||
(await this.confirm(
|
||||
'Would you like to continue with the migration regardless?',
|
||||
));
|
||||
|
||||
if (!shouldContinuePreChecks) {
|
||||
this.error('Cancelled migration.');
|
||||
}
|
||||
} else {
|
||||
debug('Soft pre-checks before migration failed:', response.errText);
|
||||
}
|
||||
} finally {
|
||||
this.stopSpinner();
|
||||
}
|
||||
}
|
||||
|
||||
async perform() {
|
||||
const percent = this.args.percent;
|
||||
if (isNaN(percent) || percent < 1 || percent > 100) {
|
||||
this.error('`PERCENT` must be a number between 1 and 100.');
|
||||
}
|
||||
|
||||
const account = this.flags.account;
|
||||
const user = this.flags.user;
|
||||
|
||||
const fromVersion = this.args.fromVersion;
|
||||
const toVersion = this.args.toVersion;
|
||||
let flagType;
|
||||
|
||||
if (user || account) {
|
||||
flagType = user ? 'user' : 'account';
|
||||
}
|
||||
|
||||
if (user && account) {
|
||||
this.error(
|
||||
'Cannot specify both `--user` and `--account`. Use only one or the other.',
|
||||
);
|
||||
}
|
||||
|
||||
if ((user || account) && percent !== 100) {
|
||||
this.error(
|
||||
`Cannot specify both \`PERCENT\` and \`--${flagType}\`. Use only one or the other.`,
|
||||
);
|
||||
}
|
||||
|
||||
const app = await this.getWritableApp();
|
||||
|
||||
let promoteFirst = false;
|
||||
if (
|
||||
percent === 100 &&
|
||||
!user &&
|
||||
!account &&
|
||||
(app.public || app.public_ish) &&
|
||||
toVersion !== app.latest_version
|
||||
) {
|
||||
this.log(
|
||||
`You're trying to migrate all the users to ${toVersion}, which is not the current production version.`,
|
||||
);
|
||||
promoteFirst = await this.confirm(
|
||||
`Do you want to promote ${toVersion} to production first?`,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
if (promoteFirst) {
|
||||
await PromoteCommand.run([toVersion, '--invokedFromAnotherCommand']);
|
||||
}
|
||||
|
||||
const body = {
|
||||
job: {
|
||||
name: 'migrate',
|
||||
from_version: fromVersion,
|
||||
to_version: toVersion,
|
||||
email: user || account,
|
||||
email_type: flagType,
|
||||
},
|
||||
};
|
||||
|
||||
await this.run_require_confirmation_pre_checks(app, body);
|
||||
|
||||
let message;
|
||||
if (user || account) {
|
||||
message = `Requesting migration from ${fromVersion} to ${toVersion} for ${user || account}`;
|
||||
} else {
|
||||
message = `Requesting migration from ${fromVersion} to ${toVersion} for ${percent}%`;
|
||||
}
|
||||
|
||||
this.startSpinner(message);
|
||||
|
||||
if (percent) {
|
||||
body.job.percent_human = percent;
|
||||
}
|
||||
|
||||
const url = `/apps/${app.id}/migrations`;
|
||||
|
||||
try {
|
||||
await callAPI(url, { method: 'POST', body });
|
||||
} catch (err) {
|
||||
this.stopSpinner({ success: false });
|
||||
throw err;
|
||||
}
|
||||
this.stopSpinner();
|
||||
|
||||
this.log(
|
||||
`\nMigration successfully queued, check ${colors.bold.underline('zapier-platform jobs')} to track the status. Migrations usually take between 5-10 minutes.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MigrateCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
user: Flags.string({
|
||||
description:
|
||||
"Migrates a user's private Zaps under the user's individual account, excluding organization accounts",
|
||||
}),
|
||||
account: Flags.string({
|
||||
description:
|
||||
"Migrates a user's private and shared Zaps under the user's individual and organization accounts",
|
||||
}),
|
||||
yes: Flags.boolean({
|
||||
char: 'y',
|
||||
description:
|
||||
'Automatically answer "yes" to any prompts. Useful if you want to avoid interactive prompts to run this command in CI.',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
MigrateCommand.args = {
|
||||
fromVersion: Args.string({
|
||||
required: true,
|
||||
description: 'The version FROM which to migrate users.',
|
||||
}),
|
||||
toVersion: Args.string({
|
||||
required: true,
|
||||
description: 'The version TO which to migrate users.',
|
||||
}),
|
||||
percent: Args.string({
|
||||
default: 100,
|
||||
description: 'Percentage (between 1 and 100) of users to migrate.',
|
||||
parse: async (input) => parseInt(input, 10),
|
||||
}),
|
||||
};
|
||||
|
||||
MigrateCommand.skipValidInstallCheck = true;
|
||||
MigrateCommand.examples = [
|
||||
'zapier-platform migrate 1.0.0 1.0.1',
|
||||
'zapier-platform migrate 1.0.1 2.0.0 10',
|
||||
'zapier-platform migrate 2.0.0 2.0.1 --user=user@example.com',
|
||||
'zapier-platform migrate 2.0.0 2.0.1 --account=account@example.com',
|
||||
];
|
||||
MigrateCommand.description = `Migrate a percentage of users or a single user from one version of your integration to another.
|
||||
|
||||
Start a migration to move users between different versions of your integration. You may also "revert" by simply swapping the from/to verion strings in the command line arguments (i.e. \`zapier-platform migrate 1.0.1 1.0.0\`).
|
||||
|
||||
**Only use this command to migrate users between non-breaking versions, use \`zapier-platform deprecate\` if you have breaking changes!**
|
||||
|
||||
Migration time varies based on the number of affected Zaps. Be patient and check \`zapier-platform jobs\` to track the status. Or use \`zapier-platform history\` if you want to see older jobs.
|
||||
|
||||
Since a migration is only for non-breaking changes, users are not emailed about the update/migration. It will be a transparent process for them.
|
||||
|
||||
We recommend migrating a small subset of users first, via the percent argument, then watching error logs of the new version for any sort of odd behavior. When you feel confident there are no bugs, go ahead and migrate everyone. If you see unexpected errors, you can revert.
|
||||
|
||||
You can migrate a specific user's Zaps by using \`--user\` (i.e. \`zapier-platform migrate 1.0.0 1.0.1 --user=user@example.com\`). This will migrate Zaps that are private for that user. Zaps that are
|
||||
|
||||
- [shared across the team](https://help.zapier.com/hc/en-us/articles/8496277647629),
|
||||
- [shared app connections](https://help.zapier.com/hc/en-us/articles/8496326497037-Share-app-connections-with-your-team), or
|
||||
- in a [team/company account](https://help.zapier.com/hc/en-us/articles/22330977078157-Collaborate-with-members-of-your-Team-or-Company-account)
|
||||
|
||||
will **not** be migrated.
|
||||
|
||||
Alternatively, you can pass the \`--account\` flag, (i.e. \`zapier-platform migrate 1.0.0 1.0.1 --account=account@example.com\`). This will migrate all Zaps owned by the user, Private & Shared, within all accounts for which the specified user is a member.
|
||||
|
||||
**The \`--account\` flag should be used cautiously as it can break shared Zaps for other users in Team or Enterprise accounts.**
|
||||
|
||||
You cannot pass both \`PERCENT\` and \`--user\` or \`--account\`.
|
||||
|
||||
You cannot pass both \`--user\` and \`--account\`.`;
|
||||
|
||||
module.exports = MigrateCommand;
|
||||
300
vendor/zapier-platform/packages/cli/src/oclif/commands/promote.js
vendored
Normal file
300
vendor/zapier-platform/packages/cli/src/oclif/commands/promote.js
vendored
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
const _ = require('lodash');
|
||||
const debug = require('debug')('zapier:promote');
|
||||
const colors = require('colors/safe');
|
||||
const { Args, Flags } = require('@oclif/core');
|
||||
|
||||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
const { callAPI } = require('../../utils/api');
|
||||
const { flattenCheckResult } = require('../../utils/display');
|
||||
const { getVersionChangelog } = require('../../utils/changelog');
|
||||
const checkMissingAppInfo = require('../../utils/check-missing-app-info');
|
||||
const { EXAMPLE_CHANGELOG } = require('../../constants');
|
||||
|
||||
const ACTION_TYPE_MAPPING = {
|
||||
read: 'trigger',
|
||||
write: 'create',
|
||||
search: 'search',
|
||||
};
|
||||
|
||||
const serializeErrors = (errors) => {
|
||||
const opener = 'Promotion failed for the following reasons:\n\n';
|
||||
if (typeof errors[0] === 'string') {
|
||||
// errors is an array of strings
|
||||
return opener + errors.map((e) => `* ${e}`).join('\n');
|
||||
}
|
||||
|
||||
const issues = flattenCheckResult({ errors });
|
||||
return (
|
||||
opener +
|
||||
issues
|
||||
.map((i) => `* ${i.method}: ${i.description}\n ${colors.gray(i.link)}`)
|
||||
.join('\n')
|
||||
);
|
||||
};
|
||||
|
||||
const hasAppChangeType = (metadata, changeType) => {
|
||||
return Boolean(
|
||||
metadata?.some(
|
||||
// Existing property name
|
||||
// eslint-disable-next-line camelcase
|
||||
({ app_change_type }) => app_change_type === changeType,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
class PromoteCommand extends BaseCommand {
|
||||
async run_require_confirmation_pre_checks(app, requestBody) {
|
||||
const assumeYes = 'yes' in this.flags;
|
||||
const url = `/apps/${app.id}/pre-migration-require-confirmation-checks`;
|
||||
|
||||
this.startSpinner(`Running pre-checks before promoting...`);
|
||||
|
||||
try {
|
||||
await callAPI(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
body: requestBody,
|
||||
},
|
||||
true,
|
||||
);
|
||||
} catch (response) {
|
||||
this.stopSpinner({ success: false });
|
||||
// 409 from the backend specifically signals pre-checks failed
|
||||
if (response.status === 409) {
|
||||
const softCheckErrors = _.get(response, 'json.errors', []);
|
||||
const formattedErrors = softCheckErrors.map((e) => `* ${e}`).join('\n');
|
||||
|
||||
this.log();
|
||||
this.log(
|
||||
'Non-blocking checks prior to promoting the integration returned warnings:',
|
||||
);
|
||||
this.log(formattedErrors);
|
||||
this.log();
|
||||
|
||||
const shouldContinuePreChecks =
|
||||
assumeYes ||
|
||||
(await this.confirm(
|
||||
'Would you like to continue with the promotion process regardless?',
|
||||
));
|
||||
|
||||
if (!shouldContinuePreChecks) {
|
||||
this.error('Cancelled promote.');
|
||||
}
|
||||
} else {
|
||||
debug('Soft pre-checks before promotion failed:', response.errText);
|
||||
}
|
||||
} finally {
|
||||
this.stopSpinner();
|
||||
}
|
||||
}
|
||||
|
||||
async perform() {
|
||||
const app = await this.getWritableApp();
|
||||
|
||||
checkMissingAppInfo(app);
|
||||
|
||||
const version = this.args.version;
|
||||
const assumeYes = 'yes' in this.flags;
|
||||
|
||||
let shouldContinueChangelog;
|
||||
|
||||
const { changelog, appMetadata, issueMetadata } =
|
||||
await getVersionChangelog(version);
|
||||
|
||||
const metadataPromptHelper = `Issues are indicated by ${colors.bold.underline(
|
||||
'#<issueId>',
|
||||
)}, and actions by ${colors.bold.underline(
|
||||
'<trigger|create|search>/<key>',
|
||||
)}. Note issue IDs must be numeric and action identifiers are case sensitive.`;
|
||||
|
||||
if (!changelog) {
|
||||
this.error(`${colors.yellow(
|
||||
'Warning!',
|
||||
)} Changelog not found. Please create a CHANGELOG.md file with user-facing descriptions. Example:
|
||||
${colors.cyan(EXAMPLE_CHANGELOG)}
|
||||
If bugfixes or updates to actions are present, then should be marked on a line that begins with "Update" or "Fix" (case insensitive) and information that contains the identifier.
|
||||
${metadataPromptHelper}`);
|
||||
} else {
|
||||
this.log(colors.green(`Changelog found for ${version}`));
|
||||
this.log(`\n---\n${changelog}\n---`);
|
||||
/* eslint-disable camelcase */
|
||||
this.log(`\nParsed metadata:\n`);
|
||||
|
||||
const appFeatureUpdates =
|
||||
appMetadata &&
|
||||
appMetadata
|
||||
.filter(({ app_change_type }) => app_change_type === 'FEATURE_UPDATE')
|
||||
.map(
|
||||
({ action_type, action_key }) =>
|
||||
`${action_key}/${ACTION_TYPE_MAPPING[action_type]}`,
|
||||
);
|
||||
|
||||
const issueFeatureUpdates =
|
||||
issueMetadata &&
|
||||
issueMetadata
|
||||
.filter(({ app_change_type }) => app_change_type === 'FEATURE_UPDATE')
|
||||
.map(({ issue_id }) => `#${issue_id}`);
|
||||
|
||||
if (appFeatureUpdates || issueFeatureUpdates) {
|
||||
this.log(
|
||||
`Feature updates: ${[
|
||||
...(appFeatureUpdates ?? []),
|
||||
...(issueFeatureUpdates ?? []),
|
||||
].join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
const appBugfixes =
|
||||
appMetadata &&
|
||||
appMetadata
|
||||
.filter(({ app_change_type }) => app_change_type === 'BUGFIX')
|
||||
.map(
|
||||
({ action_type, action_key }) =>
|
||||
`${action_key}/${ACTION_TYPE_MAPPING[action_type]}`,
|
||||
);
|
||||
const issueBugfixes =
|
||||
issueMetadata &&
|
||||
issueMetadata
|
||||
.filter(({ app_change_type }) => app_change_type === 'BUGFIX')
|
||||
.map(({ issue_id }) => `#${issue_id}`);
|
||||
|
||||
if (appBugfixes || issueBugfixes) {
|
||||
this.log(
|
||||
`Bug fixes: ${[...(appBugfixes ?? []), ...(issueBugfixes ?? [])].join(
|
||||
', ',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!appFeatureUpdates &&
|
||||
!issueFeatureUpdates &&
|
||||
!appBugfixes &&
|
||||
!issueBugfixes
|
||||
) {
|
||||
this.log(
|
||||
`No metadata was found in the changelog. Remember, you can associate the changelog with issues or triggers/actions.\n\n${metadataPromptHelper}`,
|
||||
);
|
||||
}
|
||||
this.log();
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
shouldContinueChangelog =
|
||||
assumeYes ||
|
||||
(await this.confirm(
|
||||
'Would you like to continue promoting with this changelog?',
|
||||
));
|
||||
|
||||
if (!shouldContinueChangelog) {
|
||||
this.error('Cancelled promote.');
|
||||
}
|
||||
}
|
||||
|
||||
this.log(
|
||||
`Preparing to promote version ${version} of your integration "${app.title}".`,
|
||||
);
|
||||
|
||||
const isFeatureUpdate =
|
||||
hasAppChangeType(appMetadata, 'FEATURE_UPDATE') ||
|
||||
hasAppChangeType(issueMetadata, 'FEATURE_UPDATE');
|
||||
const isBugfix =
|
||||
hasAppChangeType(appMetadata, 'BUGFIX') ||
|
||||
hasAppChangeType(issueMetadata, 'BUGFIX');
|
||||
const body = {
|
||||
job: {
|
||||
name: 'promote',
|
||||
to_version: version,
|
||||
changelog,
|
||||
app_metadata: appMetadata,
|
||||
loki_metadata: issueMetadata,
|
||||
is_feature_update: isFeatureUpdate,
|
||||
is_bugfix: isBugfix,
|
||||
is_other: !isFeatureUpdate && !isBugfix,
|
||||
},
|
||||
};
|
||||
|
||||
await this.run_require_confirmation_pre_checks(app, body);
|
||||
|
||||
this.startSpinner(`Verifying and promoting ${version}`);
|
||||
|
||||
const url = `/apps/${app.id}/migrations`;
|
||||
try {
|
||||
await callAPI(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
body,
|
||||
},
|
||||
true,
|
||||
);
|
||||
} catch (response) {
|
||||
const activationUrl = _.get(response, ['json', 'activationInfo', 'url']);
|
||||
if (activationUrl) {
|
||||
this.stopSpinner();
|
||||
this.log('\nGood news! Your integration passes validation.');
|
||||
this.log(
|
||||
`The next step is to visit ${colors.cyan(
|
||||
activationUrl,
|
||||
)} to request to publish your integration.`,
|
||||
);
|
||||
} else {
|
||||
this.stopSpinner({ success: false });
|
||||
|
||||
const errors = _.get(response, 'json.errors');
|
||||
if (!_.isEmpty(errors)) {
|
||||
this.error(serializeErrors(errors));
|
||||
} else if (response.errText) {
|
||||
this.error(response.errText);
|
||||
} else {
|
||||
// is an actual error
|
||||
this.error(response);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.stopSpinner();
|
||||
this.log(' Promotion successful!');
|
||||
}
|
||||
}
|
||||
|
||||
PromoteCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
yes: Flags.boolean({
|
||||
char: 'y',
|
||||
description:
|
||||
'Automatically answer "yes" to any prompts. Useful if you want to avoid interactive prompts to run this command in CI.',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
PromoteCommand.args = {
|
||||
version: Args.string({
|
||||
required: true,
|
||||
description: 'The version you want to promote.',
|
||||
}),
|
||||
};
|
||||
|
||||
PromoteCommand.skipValidInstallCheck = true;
|
||||
PromoteCommand.examples = ['zapier-platform promote 1.0.0'];
|
||||
PromoteCommand.description = `Promote a specific version to public access.
|
||||
|
||||
Promote an integration version into production (non-private) rotation, which means new users can use this integration version.
|
||||
|
||||
* This **does** mark the version as the official public version - all other versions & users are grandfathered.
|
||||
* This does **NOT** build/upload or deploy a version to Zapier - you should \`zapier-platform push\` first.
|
||||
* This does **NOT** move old users over to this version - \`zapier-platform migrate 1.0.0 1.0.1\` does that.
|
||||
* This does **NOT** recommend old users stop using this version - \`zapier-platform deprecate 1.0.0 2017-01-01\` does that.
|
||||
|
||||
Promotes are an inherently safe operation for all existing users of your integration.
|
||||
|
||||
After a promotion, go to your developer platform to [close issues that were resolved](https://platform.zapier.com/manage/user-feedback#3-close-resolved-issues) in the updated version.
|
||||
|
||||
If your integration is private and passes our integration checks, this will give you a URL to a form where you can fill in additional information for your integration to go public. After reviewing, the Zapier team will approve to make it public if there are no issues or decline with feedback.
|
||||
|
||||
Check \`zapier-platform jobs\` to track the status of the promotion. Or use \`zapier-platform history\` if you want to see older jobs.`;
|
||||
|
||||
module.exports = PromoteCommand;
|
||||
69
vendor/zapier-platform/packages/cli/src/oclif/commands/pull.js
vendored
Normal file
69
vendor/zapier-platform/packages/cli/src/oclif/commands/pull.js
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
const AdmZip = require('adm-zip');
|
||||
const { ensureFileSync } = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { createEnv } = require('../../utils/esm-wrapper');
|
||||
|
||||
const ZapierBaseCommand = require('../ZapierBaseCommand');
|
||||
const { downloadSourceZip } = require('../../utils/api');
|
||||
const { ensureDir, makeTempDir, removeDirSync } = require('../../utils/files');
|
||||
const { walkDirWithPresetBlocklist } = require('../../utils/build');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
const PullGeneratorPromise = require('../../generators/pull');
|
||||
|
||||
const listFiles = (dir) => {
|
||||
const relPaths = [];
|
||||
for (const entry of walkDirWithPresetBlocklist(dir)) {
|
||||
relPaths.push(path.join(path.relative(dir, entry.parentPath), entry.name));
|
||||
}
|
||||
return relPaths;
|
||||
};
|
||||
|
||||
class PullCommand extends ZapierBaseCommand {
|
||||
async perform() {
|
||||
// Fetch the source zip from API
|
||||
const tmpDir = makeTempDir();
|
||||
const srcZipDst = path.join(tmpDir, 'download', 'source.zip');
|
||||
|
||||
try {
|
||||
ensureFileSync(srcZipDst);
|
||||
await downloadSourceZip(srcZipDst);
|
||||
|
||||
// Write source zip to tmp dir
|
||||
const srcDst = path.join(tmpDir, 'source');
|
||||
await ensureDir(srcDst);
|
||||
const zip = new AdmZip(srcZipDst);
|
||||
zip.extractAllTo(srcDst, true);
|
||||
|
||||
// Prompt user to confirm overwrite
|
||||
const currentDir = process.cwd();
|
||||
const sourceFiles = listFiles(srcDst);
|
||||
|
||||
const env = await createEnv(); // await needed because createEnv() uses dynamic import() for ESM-only yeoman-environment
|
||||
const PullGenerator = await PullGeneratorPromise; // await needed because generator classes are now created via ESM dynamic import
|
||||
const namespace = 'zapier:pull';
|
||||
env.registerStub(PullGenerator, namespace);
|
||||
await env.run(namespace, {
|
||||
sourceFiles,
|
||||
srcDir: srcDst,
|
||||
dstDir: currentDir,
|
||||
});
|
||||
} finally {
|
||||
removeDirSync(tmpDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PullCommand.flags = buildFlags();
|
||||
PullCommand.description = `Retrieve and update your local integration files with the promoted version (or latest version if not public).
|
||||
|
||||
This command updates your local integration files with the promoted version (or latest version if not public). You will be prompted with a confirmation dialog before continuing if there any destructive file changes.
|
||||
|
||||
Zapier may release new versions of your integration with bug fixes or new features. In the event this occurs, you will be unable to do the following until your local files are updated by running \`zapier-platform pull\`:
|
||||
|
||||
* push to the promoted version
|
||||
* promote a new version
|
||||
* migrate users from one version to another`;
|
||||
|
||||
PullCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = PullCommand;
|
||||
67
vendor/zapier-platform/packages/cli/src/oclif/commands/push.js
vendored
Normal file
67
vendor/zapier-platform/packages/cli/src/oclif/commands/push.js
vendored
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
const ZapierBaseCommand = require('../ZapierBaseCommand');
|
||||
const { BUILD_PATH, SOURCE_PATH } = require('../../constants');
|
||||
const { Flags } = require('@oclif/core');
|
||||
const colors = require('colors/safe');
|
||||
|
||||
const BuildCommand = require('./build');
|
||||
|
||||
const { buildAndOrUpload } = require('../../utils/build');
|
||||
class PushCommand extends ZapierBaseCommand {
|
||||
async perform() {
|
||||
const skipDepInstall = this.flags['skip-dep-install'];
|
||||
|
||||
const snapshotLabel = this.flags.snapshot;
|
||||
if (snapshotLabel && snapshotLabel.length > 12) {
|
||||
throw new Error('Snapshot label cannot exceed 12 characters');
|
||||
}
|
||||
|
||||
const snapshotVersion = snapshotLabel
|
||||
? `0.0.0-${snapshotLabel}`
|
||||
: undefined;
|
||||
|
||||
await buildAndOrUpload(
|
||||
{ build: true, upload: true },
|
||||
{
|
||||
skipDepInstall,
|
||||
disableDependencyDetection: this.flags['disable-dependency-detection'],
|
||||
skipValidation: this.flags['skip-validation'],
|
||||
overwritePartnerChanges: this.flags['overwrite-partner-changes'],
|
||||
},
|
||||
snapshotVersion,
|
||||
);
|
||||
this.log(
|
||||
`\nPush complete! Built ${BUILD_PATH} and ${SOURCE_PATH} and uploaded them to Zapier.`,
|
||||
);
|
||||
this.log(
|
||||
`Now you can test it using ${colors.bold.underline('zapier-platform invoke -r')}.`,
|
||||
);
|
||||
|
||||
if (!skipDepInstall) {
|
||||
this.log(
|
||||
`\nTip: Try ${colors.bold.underline('zapier-platform push --skip-dep-install')} for faster builds.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PushCommand.flags = {
|
||||
...BuildCommand.flags,
|
||||
'overwrite-partner-changes': Flags.boolean({
|
||||
description:
|
||||
'(Internal Use Only) Allows Zapier Staff to push changes to integrations in certain situations.',
|
||||
hidden: true,
|
||||
}),
|
||||
snapshot: Flags.string({
|
||||
description:
|
||||
'Pass in a label to create a snapshot version of this integration for development and testing purposes. The version will be created as: 0.0.0-MY-LABEL',
|
||||
}),
|
||||
};
|
||||
PushCommand.examples = [
|
||||
'zapier-platform push',
|
||||
'zapier-platform push --snapshot MY-LABEL',
|
||||
];
|
||||
PushCommand.description = `Build and upload the current integration.
|
||||
|
||||
This command is the same as running \`zapier-platform build\` and \`zapier-platform upload\` in sequence. See those for more info.`;
|
||||
|
||||
module.exports = PushCommand;
|
||||
312
vendor/zapier-platform/packages/cli/src/oclif/commands/register.js
vendored
Normal file
312
vendor/zapier-platform/packages/cli/src/oclif/commands/register.js
vendored
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
const colors = require('colors/safe');
|
||||
const { Args, Flags } = require('@oclif/core');
|
||||
|
||||
const ZapierBaseCommand = require('../ZapierBaseCommand');
|
||||
const {
|
||||
CURRENT_APP_FILE,
|
||||
MAX_DESCRIPTION_LENGTH,
|
||||
MIN_TITLE_LENGTH,
|
||||
} = require('../../constants');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
const {
|
||||
callAPI,
|
||||
getLinkedAppConfig,
|
||||
getWritableApp,
|
||||
isPublished,
|
||||
writeLinkedAppConfig,
|
||||
} = require('../../utils/api');
|
||||
|
||||
class RegisterCommand extends ZapierBaseCommand {
|
||||
/**
|
||||
* Entry point function that runs when user runs `zapier-platform register`
|
||||
*/
|
||||
async perform() {
|
||||
// Flag validation
|
||||
this._validateEnumFlags();
|
||||
|
||||
if (
|
||||
'desc' in this.flags &&
|
||||
this.flags.desc.length > MAX_DESCRIPTION_LENGTH
|
||||
) {
|
||||
throw new Error(
|
||||
`Please provide a description that is ${MAX_DESCRIPTION_LENGTH} characters or less.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
this.args.title !== undefined &&
|
||||
this.args.title.length < MIN_TITLE_LENGTH
|
||||
) {
|
||||
throw new Error(
|
||||
`Please provide a title that is ${MIN_TITLE_LENGTH} characters or more.`,
|
||||
);
|
||||
}
|
||||
|
||||
const { appMeta, action } = await this._promptForAppMeta();
|
||||
|
||||
switch (action) {
|
||||
case 'update': {
|
||||
this.startSpinner(
|
||||
`Updating your existing integration "${appMeta.title}"`,
|
||||
);
|
||||
await callAPI(`/apps/${this.app.id}`, {
|
||||
method: 'PUT',
|
||||
body: appMeta,
|
||||
});
|
||||
this.stopSpinner();
|
||||
this.log('\nIntegration successfully updated!');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'register': {
|
||||
this.startSpinner(
|
||||
`Registering your new integration "${appMeta.title}"`,
|
||||
);
|
||||
const app = await callAPI('/apps?formId=create', {
|
||||
method: 'POST',
|
||||
body: appMeta,
|
||||
});
|
||||
this.stopSpinner();
|
||||
this.startSpinner(
|
||||
`Linking app to current directory with \`${CURRENT_APP_FILE}\``,
|
||||
);
|
||||
await writeLinkedAppConfig(app, process.cwd());
|
||||
this.stopSpinner();
|
||||
this.log(
|
||||
'\nFinished! Now that your integration is registered with Zapier, you can `zapier-platform push`!',
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates values provided for enum flags against options retrieved from the BE
|
||||
* (see getAppRegistrationFieldChoices hook for more details)
|
||||
*/
|
||||
_validateEnumFlags() {
|
||||
const flagFieldMappings = {
|
||||
audience: 'intention',
|
||||
role: 'role',
|
||||
category: 'app_category',
|
||||
};
|
||||
|
||||
for (const [flag, flagValue] of Object.entries(this.flags)) {
|
||||
// Only validate user input for enum flags (in flagFieldMappings)
|
||||
if (!flagFieldMappings[flag]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check user input against this.config.enumFieldChoices (retrieved in getAppRegistrationFieldChoices hook)
|
||||
const enumFieldChoices =
|
||||
this.config.enumFieldChoices[flagFieldMappings[flag]];
|
||||
if (!enumFieldChoices.find((option) => option.value === flagValue)) {
|
||||
throw new Error(
|
||||
`${flagValue} is not a valid value for ${flag}. Must be one of the following: ${enumFieldChoices
|
||||
.map((option) => option.value)
|
||||
.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts user for values that have not been provided
|
||||
* Flags can heavily impact the behavior of this function
|
||||
* @returns { appMeta: {object}, action: string }
|
||||
*/
|
||||
async _promptForAppMeta() {
|
||||
const appMeta = {};
|
||||
|
||||
const actionChoices = [
|
||||
{ name: 'Yes, update current integration', value: 'update' },
|
||||
{ name: 'No, register a new integration', value: 'register' },
|
||||
];
|
||||
|
||||
let action = actionChoices[1].value; // Default action is register
|
||||
|
||||
const linkedAppId = (await getLinkedAppConfig(undefined, false))?.id;
|
||||
if (linkedAppId) {
|
||||
console.info(colors.yellow(`${CURRENT_APP_FILE} file detected.`));
|
||||
if (this.flags.yes) {
|
||||
console.info(
|
||||
colors.yellow(
|
||||
`-y/--yes flag passed, updating current integration (ID: ${linkedAppId}).`,
|
||||
),
|
||||
);
|
||||
action = actionChoices[0].value;
|
||||
} else {
|
||||
action = await this.promptWithList(
|
||||
`Would you like to update your current integration (ID: ${linkedAppId})?`,
|
||||
actionChoices,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (action === 'update') {
|
||||
this.startSpinner('Retrieving details for your integration');
|
||||
this.app = await getWritableApp();
|
||||
this.stopSpinner();
|
||||
|
||||
// Block published apps from updating settings
|
||||
if (this.app?.status && isPublished(this.app.status)) {
|
||||
throw new Error(
|
||||
"You can't edit settings for this integration. To edit your integration details on Zapier's public app directory, email partners@zapier.com.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
appMeta.title = this.args.title?.trim();
|
||||
if (!appMeta.title) {
|
||||
appMeta.title = await this.prompt(
|
||||
`What is the title of your integration? It must be ${MIN_TITLE_LENGTH} characters at minimum.`,
|
||||
{
|
||||
required: true,
|
||||
charMinimum: MIN_TITLE_LENGTH,
|
||||
default: this.app?.title,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
appMeta.description = this.flags.desc?.trim();
|
||||
if (!appMeta.description) {
|
||||
appMeta.description = await this.prompt(
|
||||
`Please provide a sentence describing your app in ${MAX_DESCRIPTION_LENGTH} characters or less.`,
|
||||
{
|
||||
required: true,
|
||||
charLimit: MAX_DESCRIPTION_LENGTH,
|
||||
default: this.app?.description,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
appMeta.homepage_url = this.flags.url;
|
||||
if (!appMeta.homepage_url) {
|
||||
appMeta.homepage_url = await this.prompt(
|
||||
'What is the homepage URL of your app? (optional)',
|
||||
{ default: this.app?.homepage_url },
|
||||
);
|
||||
}
|
||||
|
||||
appMeta.intention = this.flags.audience;
|
||||
if (!appMeta.intention) {
|
||||
appMeta.intention = await this.promptWithList(
|
||||
'Are you building a public or private integration?',
|
||||
this.config.enumFieldChoices.intention,
|
||||
{ default: this.app?.intention },
|
||||
);
|
||||
}
|
||||
|
||||
appMeta.role = this.flags.role;
|
||||
if (!appMeta.role) {
|
||||
appMeta.role = await this.promptWithList(
|
||||
"What is your relationship with the app you're integrating with Zapier?",
|
||||
this._getRoleChoicesWithAppTitle(
|
||||
appMeta.title,
|
||||
this.config.enumFieldChoices.role,
|
||||
),
|
||||
{ default: this.app?.role },
|
||||
);
|
||||
}
|
||||
|
||||
appMeta.app_category = this.flags.category;
|
||||
if (!appMeta.app_category) {
|
||||
appMeta.app_category = await this.promptWithList(
|
||||
'How would you categorize your app?',
|
||||
this.config.enumFieldChoices.app_category,
|
||||
{ default: this.app?.app_category },
|
||||
);
|
||||
}
|
||||
|
||||
if (action === 'register') {
|
||||
appMeta.subscription = this.flags.subscribe;
|
||||
if (typeof this.flags.yes !== 'undefined') {
|
||||
appMeta.subscription = true;
|
||||
} else if (typeof appMeta.subscription === 'undefined') {
|
||||
// boolean field, so using `typeof` === `undefined`
|
||||
appMeta.subscription = await this.promptWithList(
|
||||
'Subscribe to Updates about your Integration',
|
||||
[
|
||||
{ name: 'Yes', value: true },
|
||||
{ name: 'No', value: false },
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { appMeta, action };
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} title title of integration
|
||||
* @param {array} choices retrieved role choices with `[app_title]` tokens
|
||||
* @returns {array} array of choices with integration titles (instead of `[app_title]` tokens)
|
||||
*/
|
||||
_getRoleChoicesWithAppTitle(title, choices) {
|
||||
return choices.map((choice) => ({
|
||||
value: choice.value,
|
||||
name: choice.name.replace('[app_title]', title),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
RegisterCommand.skipValidInstallCheck = true;
|
||||
RegisterCommand.args = {
|
||||
title: Args.string({
|
||||
description:
|
||||
"Your integration's public title. Asked interactively if not present.",
|
||||
}),
|
||||
};
|
||||
|
||||
RegisterCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
desc: Flags.string({
|
||||
char: 'D',
|
||||
description: `A sentence describing your app in ${MAX_DESCRIPTION_LENGTH} characters or less, e.g. "Trello is a team collaboration tool to organize tasks and keep projects on track."`,
|
||||
}),
|
||||
url: Flags.string({
|
||||
char: 'u',
|
||||
description: 'The homepage URL of your app, e.g., https://example.com.',
|
||||
}),
|
||||
audience: Flags.string({
|
||||
char: 'a',
|
||||
description: 'Are you building a public or private integration?',
|
||||
}),
|
||||
role: Flags.string({
|
||||
char: 'r',
|
||||
description:
|
||||
"What is your relationship with the app you're integrating with Zapier?",
|
||||
}),
|
||||
category: Flags.string({
|
||||
char: 'c',
|
||||
description:
|
||||
"How would you categorize your app? Choose the most appropriate option for your app's core features.",
|
||||
}),
|
||||
subscribe: Flags.boolean({
|
||||
char: 's',
|
||||
description:
|
||||
'Get tips and recommendations about this integration along with our monthly newsletter that details the performance of your integration and the latest Zapier news.',
|
||||
allowNo: true,
|
||||
}),
|
||||
yes: Flags.boolean({
|
||||
char: 'y',
|
||||
description:
|
||||
'Assume yes for all yes/no prompts. This flag will also update an existing integration (as opposed to registering a new one) if a .zapierapprc file is found.',
|
||||
}),
|
||||
},
|
||||
});
|
||||
RegisterCommand.examples = [
|
||||
'zapier-platform register',
|
||||
'zapier-platform register "My Cool Integration"',
|
||||
'zapier-platform register "My Cool Integration" --desc "My Cool Integration helps you integrate your apps with the apps that you need." --no-subscribe',
|
||||
'zapier-platform register "My Cool Integration" --url "https://www.zapier.com" --audience private --role employee --category marketing-automation',
|
||||
'zapier-platform register --subscribe',
|
||||
];
|
||||
RegisterCommand.description = `Register a new integration in your account, or update the existing one if a \`${CURRENT_APP_FILE}\` file is found.
|
||||
|
||||
This command creates a new integration and links it in the \`./${CURRENT_APP_FILE}\` file. If \`${CURRENT_APP_FILE}\` already exists, it will ask you if you want to update the currently-linked integration, as opposed to creating a new one.
|
||||
|
||||
After registering a new integration, you can run \`zapier-platform push\` to build and upload your integration for use in the Zapier editor. This will change \`${CURRENT_APP_FILE}\`, which identifies this directory as holding code for a specific integration.`;
|
||||
|
||||
module.exports = RegisterCommand;
|
||||
225
vendor/zapier-platform/packages/cli/src/oclif/commands/scaffold.js
vendored
Normal file
225
vendor/zapier-platform/packages/cli/src/oclif/commands/scaffold.js
vendored
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
// @ts-check
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const { Args, Flags } = require('@oclif/core');
|
||||
|
||||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
|
||||
const {
|
||||
createScaffoldingContext,
|
||||
plural,
|
||||
updateEntryFile,
|
||||
isValidEntryFileUpdate,
|
||||
writeTemplateFile,
|
||||
} = require('../../utils/scaffold');
|
||||
const { splitFileFromPath } = require('../../utils/string');
|
||||
const { isValidAppInstall } = require('../../utils/misc');
|
||||
const { writeFile } = require('../../utils/files');
|
||||
const { ISSUES_URL } = require('../../constants');
|
||||
|
||||
class ScaffoldCommand extends BaseCommand {
|
||||
async perform() {
|
||||
const { actionType, noun } = this.args;
|
||||
const indexFileLocal = this.flags.entry ?? this.defaultIndexFileLocal();
|
||||
const {
|
||||
dest: actionDirLocal = this.defaultActionDirLocal(indexFileLocal),
|
||||
'test-dest': testDirLocal = this.defaultTestDirLocal(indexFileLocal),
|
||||
force,
|
||||
} = this.flags;
|
||||
|
||||
const language = indexFileLocal.endsWith('.ts') ? 'ts' : 'js';
|
||||
|
||||
const context = createScaffoldingContext({
|
||||
actionType,
|
||||
noun,
|
||||
language,
|
||||
indexFileLocal,
|
||||
actionDirLocal,
|
||||
testDirLocal,
|
||||
includeIntroComments: !this.flags['no-help'],
|
||||
preventOverwrite: !force,
|
||||
});
|
||||
|
||||
// TODO: read from config file?
|
||||
|
||||
this.startSpinner(`Creating new file: ${context.actionFileLocal}`);
|
||||
|
||||
await writeTemplateFile({
|
||||
destinationPath: context.actionFileResolved,
|
||||
templateType: context.actionType,
|
||||
language: context.language,
|
||||
preventOverwrite: context.preventOverwrite,
|
||||
templateContext: context.templateContext,
|
||||
});
|
||||
this.stopSpinner();
|
||||
|
||||
this.startSpinner(`Creating new test file: ${context.testFileLocal}`);
|
||||
await writeTemplateFile({
|
||||
destinationPath: context.testFileResolved,
|
||||
templateType: 'test',
|
||||
language: context.language,
|
||||
preventOverwrite: context.preventOverwrite,
|
||||
templateContext: context.templateContext,
|
||||
});
|
||||
this.stopSpinner();
|
||||
|
||||
// * rewire the index.js to point to the new file
|
||||
this.startSpinner(`Rewriting your ${context.indexFileLocal}`);
|
||||
|
||||
const originalContents = await updateEntryFile({
|
||||
language: context.language,
|
||||
indexFileResolved: context.indexFileResolved,
|
||||
actionRelativeImportPath: context.actionRelativeImportPath,
|
||||
actionImportName: context.templateContext.VARIABLE,
|
||||
actionType: context.actionType,
|
||||
});
|
||||
|
||||
if (isValidAppInstall().valid) {
|
||||
const success = isValidEntryFileUpdate(
|
||||
context.language,
|
||||
context.indexFileResolved,
|
||||
context.actionType,
|
||||
context.templateContext.KEY,
|
||||
);
|
||||
|
||||
this.stopSpinner({ success });
|
||||
|
||||
if (!success) {
|
||||
const entryName = splitFileFromPath(context.indexFileResolved)[1];
|
||||
|
||||
this.startSpinner(
|
||||
`Unable to successfully rewrite your ${entryName}. Rolling back...`,
|
||||
);
|
||||
await writeFile(context.indexFileResolved, originalContents);
|
||||
this.stopSpinner();
|
||||
|
||||
this.error(
|
||||
[
|
||||
`\nPlease add the following lines to ${context.indexFileResolved}:`,
|
||||
` * \`const ${context.templateContext.VARIABLE} = require('./${context.actionRelativeImportPath}');\` at the top-level`,
|
||||
` * \`[${context.templateContext.VARIABLE}.key]: ${context.templateContext.VARIABLE}\` in the "${context.actionTypePlural}" object in your exported integration definition.`,
|
||||
'',
|
||||
`Also, please file an issue at ${ISSUES_URL} with the contents of your ${context.indexFileResolved}.`,
|
||||
].join('\n'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.stopSpinner();
|
||||
|
||||
if (!this.flags.invokedFromAnotherCommand) {
|
||||
this.log(`\nAll done! Your new ${context.actionType} is ready to use.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If `--entry` is not provided, this will determine the path to the
|
||||
* root index file. Notably, we'll look for tsconfig.json and
|
||||
* src/index.ts first, because even TS apps have a root level plain
|
||||
* index.js that we should ignore.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
defaultIndexFileLocal() {
|
||||
const tsConfigPath = path.join(process.cwd(), 'tsconfig.json');
|
||||
const srcIndexTsPath = path.join(process.cwd(), 'src', 'index.ts');
|
||||
if (fs.existsSync(tsConfigPath) && fs.existsSync(srcIndexTsPath)) {
|
||||
this.log('Automatically detected TypeScript project');
|
||||
return 'src/index.ts';
|
||||
}
|
||||
|
||||
return 'index.js';
|
||||
}
|
||||
|
||||
/**
|
||||
* If `--dest` is not provided, this will determine the directory for
|
||||
* the new action file to be created in.
|
||||
*
|
||||
* @param {string} indexFileLocal - The path to the index file
|
||||
* @returns {string}
|
||||
*/
|
||||
defaultActionDirLocal(indexFileLocal) {
|
||||
const parent = path.dirname(indexFileLocal);
|
||||
return path.join(parent, plural(this.args.actionType));
|
||||
}
|
||||
|
||||
/**
|
||||
* If `--test-dest` is not provided, this will determine the directory
|
||||
* for the new test file to be created in.
|
||||
*
|
||||
* @param {string} indexFileLocal - The path to the index file
|
||||
* @returns {string}
|
||||
*/
|
||||
defaultTestDirLocal(indexFileLocal) {
|
||||
const parent = path.dirname(indexFileLocal);
|
||||
return path.join(parent, 'test', plural(this.args.actionType));
|
||||
}
|
||||
}
|
||||
|
||||
ScaffoldCommand.args = {
|
||||
actionType: Args.string({
|
||||
help: 'What type of step type are you creating?',
|
||||
required: true,
|
||||
options: ['trigger', 'search', 'create', 'resource'],
|
||||
}),
|
||||
noun: Args.string({
|
||||
help: 'What sort of object this action acts on. For example, the name of the new thing to create',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
ScaffoldCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
dest: Flags.string({
|
||||
char: 'd',
|
||||
description:
|
||||
"Specify the new file's directory. Use this flag when you want to create a different folder structure such as `src/triggers` instead of the default `triggers`. Defaults to `[triggers|searches|creates]/{noun}`.",
|
||||
}),
|
||||
'test-dest': Flags.string({
|
||||
description:
|
||||
"Specify the new test file's directory. Use this flag when you want to create a different folder structure such as `src/triggers` instead of the default `triggers`. Defaults to `test/[triggers|searches|creates]/{noun}`.",
|
||||
}),
|
||||
entry: Flags.string({
|
||||
char: 'e',
|
||||
description:
|
||||
"Supply the path to your integration's entry point (`index.js` or `src/index.ts`). This will try to automatically detect the correct file if not provided.",
|
||||
}),
|
||||
force: Flags.boolean({
|
||||
char: 'f',
|
||||
description:
|
||||
'Should we overwrite an existing trigger/search/create file?',
|
||||
default: false,
|
||||
}),
|
||||
'no-help': Flags.boolean({
|
||||
description:
|
||||
"When scaffolding, should we skip adding helpful intro comments? Useful if this isn't your first rodeo.",
|
||||
default: false,
|
||||
}),
|
||||
// TODO: typescript? jscodeshift supports it. We could tweak a template for it
|
||||
},
|
||||
});
|
||||
|
||||
ScaffoldCommand.examples = [
|
||||
'zapier-platform scaffold trigger contact',
|
||||
'zapier-platform scaffold search contact --dest=my_src/searches',
|
||||
'zapier-platform scaffold create contact --entry=src/index.js',
|
||||
'zapier-platform scaffold resource contact --force',
|
||||
];
|
||||
|
||||
ScaffoldCommand.description = `Add a starting trigger, create, search, or resource to your integration.
|
||||
|
||||
The first argument should be one of \`trigger|search|create|resource\` followed by the noun that this will act on (something like "contact" or "deal").
|
||||
|
||||
The scaffold command does two general things:
|
||||
|
||||
* Creates a new file (such as \`triggers/contact.js\`)
|
||||
* Imports and registers it inside your \`index.js\`
|
||||
|
||||
You can mix and match several options to customize the created scaffold for your project.`;
|
||||
|
||||
ScaffoldCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = ScaffoldCommand;
|
||||
93
vendor/zapier-platform/packages/cli/src/oclif/commands/team/add.js
vendored
Normal file
93
vendor/zapier-platform/packages/cli/src/oclif/commands/team/add.js
vendored
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
const ZapierBaseCommand = require('../../ZapierBaseCommand');
|
||||
const { Args } = require('@oclif/core');
|
||||
const { cyan } = require('colors/safe');
|
||||
const { buildFlags } = require('../../buildFlags');
|
||||
const { callAPI } = require('../../../utils/api');
|
||||
const { BASE_ENDPOINT } = require('../../../constants');
|
||||
|
||||
const inviteMessage = (role, title) => {
|
||||
switch (role) {
|
||||
case 'admin':
|
||||
return `I would like you to help manage ${title}'s Zapier integration and get access to see how it's performing.`;
|
||||
case 'collaborator':
|
||||
return `I would like you to view ${title}'s Zapier integration and get access to see how it's performing.`;
|
||||
case 'subscriber':
|
||||
return `I would like you to get reports and updates about ${title}'s Zapier integration.`;
|
||||
}
|
||||
};
|
||||
|
||||
class TeamAddCommand extends ZapierBaseCommand {
|
||||
async perform() {
|
||||
const { id, title } = await this.getWritableApp();
|
||||
|
||||
const role = this.args.role;
|
||||
const message = this.args.message || inviteMessage(role, title);
|
||||
|
||||
if (
|
||||
!this.flags.force &&
|
||||
!(await this.confirm(
|
||||
`About to invite ${cyan(this.args.email)} to as a team member at the ${
|
||||
this.args.role
|
||||
} level. An email will be sent with the following message:\n\n"${message}"\n\nIs that ok?`,
|
||||
true,
|
||||
))
|
||||
) {
|
||||
this.log('\ncancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
this.startSpinner('Inviting team member');
|
||||
|
||||
const url =
|
||||
role === 'admin'
|
||||
? `/apps/${id}/collaborators`
|
||||
: role === 'subscriber'
|
||||
? `${BASE_ENDPOINT}/api/platform/v3/integrations/${id}/subscribers`
|
||||
: `/apps/${id}/limited_collaborators`;
|
||||
|
||||
await callAPI(url, {
|
||||
url: url.startsWith('http') ? url : undefined,
|
||||
method: 'POST',
|
||||
body: { email: this.args.email, message },
|
||||
});
|
||||
this.stopSpinner();
|
||||
}
|
||||
}
|
||||
|
||||
TeamAddCommand.args = {
|
||||
email: Args.string({
|
||||
description:
|
||||
"The user to be invited. If they don't have a Zapier account, they'll be prompted to create one.",
|
||||
required: true,
|
||||
}),
|
||||
role: Args.string({
|
||||
description:
|
||||
'The level the invited team member should be at. Admins can edit everything and get email updates. Collaborators have read-access to the app and get email updates. Subscribers only get email updates.',
|
||||
options: ['admin', 'collaborator', 'subscriber'],
|
||||
required: true,
|
||||
}),
|
||||
message: Args.string({
|
||||
description:
|
||||
'A message sent in the email to your team member, if you need to provide context. Wrap the message in quotes to ensure spaces get saved.',
|
||||
}),
|
||||
};
|
||||
TeamAddCommand.flags = buildFlags();
|
||||
TeamAddCommand.description = `Add a team member to your integration.
|
||||
|
||||
These users come in three levels:
|
||||
|
||||
* \`admin\`, who can edit everything about the integration
|
||||
* \`collaborator\`, who has read-only access for the app, and will receive periodic email updates. These updates include quarterly health scores and more.
|
||||
* \`subscriber\`, who can't directly access the app, but will receive periodic email updates. These updates include quarterly health scores and more.
|
||||
|
||||
Team members can be freely added and removed.`;
|
||||
|
||||
TeamAddCommand.examples = [
|
||||
'zapier-platform team:add bruce@wayne.com admin',
|
||||
'zapier-platform team:add robin@wayne.com collaborator "Hey Robin, check out this app."',
|
||||
'zapier-platform team:add alfred@wayne.com subscriber "Hey Alfred, check out this app."',
|
||||
];
|
||||
TeamAddCommand.aliases = ['team:invite'];
|
||||
TeamAddCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = TeamAddCommand;
|
||||
57
vendor/zapier-platform/packages/cli/src/oclif/commands/team/get.js
vendored
Normal file
57
vendor/zapier-platform/packages/cli/src/oclif/commands/team/get.js
vendored
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
const ZapierBaseCommand = require('../../ZapierBaseCommand');
|
||||
const { cyan } = require('colors/safe');
|
||||
const { listTeamMembers } = require('../../../utils/team');
|
||||
const { buildFlags } = require('../../buildFlags');
|
||||
const { transformUserRole } = require('../../../utils/team');
|
||||
|
||||
class TeamListCommand extends ZapierBaseCommand {
|
||||
async perform() {
|
||||
this.startSpinner('Loading team members');
|
||||
const { admins, limitedCollaborators, subscribers } =
|
||||
await listTeamMembers();
|
||||
this.stopSpinner();
|
||||
|
||||
const cleanedUsers = [
|
||||
...admins,
|
||||
...limitedCollaborators,
|
||||
...subscribers,
|
||||
].map(({ status, name, role, email }) => ({
|
||||
status,
|
||||
name,
|
||||
role: transformUserRole(role),
|
||||
email,
|
||||
}));
|
||||
|
||||
this.logTable({
|
||||
rows: cleanedUsers,
|
||||
headers: [
|
||||
['Name', 'name'],
|
||||
['Role', 'role'],
|
||||
['Status', 'status'],
|
||||
['Email', 'email'],
|
||||
],
|
||||
});
|
||||
|
||||
this.log(
|
||||
`To invite more team members, use the \`${cyan(
|
||||
'zapier-platform team:add',
|
||||
)}\` command.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TeamListCommand.flags = buildFlags({ opts: { format: true } });
|
||||
TeamListCommand.description = `Get team members involved with your integration.
|
||||
|
||||
These users come in three levels:
|
||||
|
||||
* \`admin\`, who can edit everything about the integration
|
||||
* \`collaborator\`, who has read-only access for the app, and will receive periodic email updates. These updates include quarterly health scores and more.
|
||||
* \`subscriber\`, who can't directly access the app, but will receive periodic email updates. These updates include quarterly health scores and more.
|
||||
|
||||
Use the \`zapier-platform team:add\` and \`zapier-platform team:remove\` commands to modify your team.
|
||||
`;
|
||||
TeamListCommand.aliases = ['team:list'];
|
||||
TeamListCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = TeamListCommand;
|
||||
75
vendor/zapier-platform/packages/cli/src/oclif/commands/team/remove.js
vendored
Normal file
75
vendor/zapier-platform/packages/cli/src/oclif/commands/team/remove.js
vendored
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
const ZapierBaseCommand = require('../../ZapierBaseCommand');
|
||||
const { cyan } = require('colors/safe');
|
||||
const { buildFlags } = require('../../buildFlags');
|
||||
const { callAPI, getWritableApp } = require('../../../utils/api');
|
||||
const { BASE_ENDPOINT } = require('../../../constants');
|
||||
const { listTeamMembers, transformUserRole } = require('../../../utils/team');
|
||||
|
||||
class TeamRemoveCommand extends ZapierBaseCommand {
|
||||
async perform() {
|
||||
this.startSpinner('Loading team members');
|
||||
const { admins, limitedCollaborators, subscribers } =
|
||||
await listTeamMembers();
|
||||
|
||||
const choices = [...admins, ...limitedCollaborators, ...subscribers].map(
|
||||
({ status, name, role, email, id }) => ({
|
||||
status,
|
||||
value: { id, email, role: transformUserRole(role) },
|
||||
name: `${email} (${transformUserRole(role)})`,
|
||||
short: email,
|
||||
}),
|
||||
);
|
||||
|
||||
this.stopSpinner();
|
||||
|
||||
const {
|
||||
role,
|
||||
email,
|
||||
id: invitationId,
|
||||
} = await this.promptWithList(
|
||||
'Which team member do you want to remove?',
|
||||
choices,
|
||||
);
|
||||
this.log();
|
||||
if (
|
||||
!(await this.confirm(
|
||||
`About to revoke ${cyan(role)}-level access from ${cyan(
|
||||
email,
|
||||
)}. Are you sure?`,
|
||||
true,
|
||||
))
|
||||
) {
|
||||
this.log('\ncancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
this.startSpinner('Removing Team Member');
|
||||
const { id: appId } = await getWritableApp();
|
||||
const url =
|
||||
role === 'admin'
|
||||
? `/apps/${appId}/collaborators/${invitationId}`
|
||||
: role === 'subscriber'
|
||||
? `${BASE_ENDPOINT}/api/platform/v3/integrations/${appId}/subscribers/${invitationId}`
|
||||
: `/apps/${appId}/limited_collaborators`;
|
||||
|
||||
await callAPI(url, {
|
||||
url: url.startsWith('http') ? url : undefined,
|
||||
method: 'DELETE',
|
||||
body: { email_id: invitationId },
|
||||
});
|
||||
|
||||
this.stopSpinner();
|
||||
}
|
||||
}
|
||||
|
||||
TeamRemoveCommand.flags = buildFlags();
|
||||
TeamRemoveCommand.description = `Remove a team member from all versions of your integration.
|
||||
|
||||
Admins will immediately lose write access to the integration.
|
||||
Collaborators will immediately lose read access to the integration.
|
||||
Subscribers won't receive future email updates.`;
|
||||
|
||||
TeamRemoveCommand.aliases = ['team:delete'];
|
||||
TeamRemoveCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = TeamRemoveCommand;
|
||||
102
vendor/zapier-platform/packages/cli/src/oclif/commands/test.js
vendored
Normal file
102
vendor/zapier-platform/packages/cli/src/oclif/commands/test.js
vendored
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
const { Flags } = require('@oclif/core');
|
||||
const chalk = require('chalk');
|
||||
|
||||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const ValidateCommand = require('./validate');
|
||||
const constants = require('../../constants');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
const { readCredentials } = require('../../utils/api');
|
||||
const { runCommand } = require('../../utils/misc');
|
||||
const { getPackageManager } = require('../../utils/package-manager');
|
||||
|
||||
class TestCommand extends BaseCommand {
|
||||
async perform() {
|
||||
if (!this.flags['skip-validate']) {
|
||||
await ValidateCommand.run(['--invokedFromAnotherCommand']);
|
||||
}
|
||||
|
||||
const extraEnv = {
|
||||
ZAPIER_BASE_ENDPOINT: constants.BASE_ENDPOINT,
|
||||
};
|
||||
|
||||
if (this.debug.enabled) {
|
||||
extraEnv.LOG_TO_STDOUT = 'true';
|
||||
extraEnv.DETAILED_LOG_TO_STDOUT = 'true';
|
||||
}
|
||||
|
||||
const credentials = await readCredentials(false);
|
||||
if (credentials.deployKey) {
|
||||
this.log(
|
||||
`Adding ${constants.AUTH_LOCATION} to environment as ZAPIER_DEPLOY_KEY...`,
|
||||
);
|
||||
extraEnv.ZAPIER_DEPLOY_KEY = credentials.deployKey;
|
||||
}
|
||||
|
||||
const env = Object.assign({}, process.env, extraEnv);
|
||||
|
||||
const packageManager = await getPackageManager(this.flags);
|
||||
|
||||
const passthroughArgs = this.argv.includes('--')
|
||||
? this.argv.slice(this.argv.indexOf('--') + 1)
|
||||
: [];
|
||||
|
||||
const argv = [
|
||||
'run',
|
||||
'--silent',
|
||||
'test',
|
||||
packageManager.useDoubleHyphenBeforeArgs ? '--' : '',
|
||||
...passthroughArgs,
|
||||
].filter(Boolean);
|
||||
|
||||
this.log('Running test suite with the following command:');
|
||||
// some extra formatting happen w/ quotes so it's clear when they're already like that in the array,
|
||||
// but the space-joined array made that unclear
|
||||
this.log(
|
||||
`\n ${chalk.cyanBright.bold(
|
||||
packageManager.executable,
|
||||
)} ${chalk.cyanBright(
|
||||
argv.map((a) => (a.includes(' ') ? `"${a}"` : a)).join(' '),
|
||||
)}\n`,
|
||||
);
|
||||
|
||||
const output = await runCommand(packageManager.executable, argv, {
|
||||
stdio: 'inherit',
|
||||
env,
|
||||
});
|
||||
if (output.stdout) {
|
||||
this.log(output.stdout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TestCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
'skip-validate': Flags.boolean({
|
||||
description:
|
||||
"Forgo running `zapier-platform validate` before tests are run. This will speed up tests if you're modifying functionality of an existing integration rather than adding new actions.",
|
||||
}),
|
||||
yarn: Flags.boolean({
|
||||
description:
|
||||
"Use `yarn` instead of `npm`. This happens automatically if there's a `yarn.lock` file, but you can manually force `yarn` if you run tests from a sub-directory.",
|
||||
}),
|
||||
pnpm: Flags.boolean({
|
||||
description:
|
||||
"Use `pnpm` instead of `npm`. This happens automatically if there's a `pnpm-lock.yaml` file, but you can manually force `pnpm` if you run tests from a sub-directory.",
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
TestCommand.skipValidInstallCheck = false;
|
||||
TestCommand.strict = false;
|
||||
TestCommand.examples = [
|
||||
'zapier-platform test',
|
||||
'zapier-platform test --skip-validate -- -t 30000 --grep api',
|
||||
'zapier-platform test -- -fo --testNamePattern "auth pass"',
|
||||
];
|
||||
TestCommand.description = `Test your integration via the "test" script in your "package.json".
|
||||
|
||||
This command is a wrapper around \`npm test\` that also validates the structure of your integration and sets up extra environment variables.
|
||||
|
||||
You can pass any args/flags after a \`--\`; they will get forwarded onto your test script.`;
|
||||
|
||||
module.exports = TestCommand;
|
||||
27
vendor/zapier-platform/packages/cli/src/oclif/commands/upload.js
vendored
Normal file
27
vendor/zapier-platform/packages/cli/src/oclif/commands/upload.js
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
|
||||
const { BUILD_PATH, SOURCE_PATH } = require('../../constants');
|
||||
|
||||
const { buildAndOrUpload } = require('../../utils/build');
|
||||
|
||||
class UploadCommand extends BaseCommand {
|
||||
async perform() {
|
||||
// it would be cool if we differentiated between new/updated here
|
||||
await buildAndOrUpload({ upload: true });
|
||||
this.log(
|
||||
`\nUpload complete! Uploaded ${BUILD_PATH} and ${SOURCE_PATH} to Zapier. If it's a new version, it should now be available in the Zap editor.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UploadCommand.skipValidInstallCheck = true;
|
||||
UploadCommand.flags = buildFlags();
|
||||
UploadCommand.description = `Upload the latest build of your integration to Zapier.
|
||||
|
||||
This command sends both ${BUILD_PATH} and ${SOURCE_PATH} to Zapier for use.
|
||||
|
||||
Typically we recommend using \`zapier-platform push\`, which does a build and upload, rather than \`upload\` by itself.
|
||||
`;
|
||||
|
||||
module.exports = UploadCommand;
|
||||
63
vendor/zapier-platform/packages/cli/src/oclif/commands/users/add.js
vendored
Normal file
63
vendor/zapier-platform/packages/cli/src/oclif/commands/users/add.js
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
const ZapierBaseCommand = require('../../ZapierBaseCommand');
|
||||
const { Args, Flags } = require('@oclif/core');
|
||||
const { cyan } = require('colors/safe');
|
||||
const { buildFlags } = require('../../buildFlags');
|
||||
const { callAPI } = require('../../../utils/api');
|
||||
|
||||
class UsersAddCommand extends ZapierBaseCommand {
|
||||
async perform() {
|
||||
if (
|
||||
!this.flags.force &&
|
||||
!(await this.confirm(
|
||||
`About to invite ${cyan(this.args.email)} to ${
|
||||
this.args.version ? `version ${this.args.version}` : 'all versions'
|
||||
} of your integration. An invite email will be sent. Is that ok?`,
|
||||
true,
|
||||
))
|
||||
) {
|
||||
this.log('\ncancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = await this.getWritableApp();
|
||||
this.startSpinner('Inviting user');
|
||||
const url = `/apps/${id}/invitees/${this.args.email}${
|
||||
this.args.version ? `/${this.args.version}` : ''
|
||||
}`;
|
||||
await callAPI(url, { method: 'POST' });
|
||||
this.stopSpinner();
|
||||
}
|
||||
}
|
||||
|
||||
UsersAddCommand.args = {
|
||||
email: Args.string({
|
||||
description:
|
||||
"The user to be invited. If they don't have a Zapier account, they'll be prompted to create one.",
|
||||
required: true,
|
||||
}),
|
||||
version: Args.string({
|
||||
description:
|
||||
'A version string (like 1.2.3). Optional, used only if you want to invite a user to a specific version instead of all versions.',
|
||||
}),
|
||||
};
|
||||
UsersAddCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
force: Flags.boolean({
|
||||
char: 'f',
|
||||
description: 'Skip confirmation. Useful for running programatically.',
|
||||
}),
|
||||
},
|
||||
});
|
||||
UsersAddCommand.examples = [
|
||||
'zapier-platform users:add bruce@wayne.com',
|
||||
'zapier-platform users:add alfred@wayne.com 1.2.3',
|
||||
];
|
||||
UsersAddCommand.description = `Add a user to some or all versions of your integration.
|
||||
|
||||
When this command is run, we'll send an email to the user inviting them to try your integration. You can track the status of that invite using the \`zapier-platform users:get\` command.
|
||||
|
||||
Invited users will be able to see your integration's name, logo, and description. They'll also be able to create Zaps using any available triggers and actions.`;
|
||||
UsersAddCommand.aliases = ['users:invite'];
|
||||
UsersAddCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = UsersAddCommand;
|
||||
55
vendor/zapier-platform/packages/cli/src/oclif/commands/users/get.js
vendored
Normal file
55
vendor/zapier-platform/packages/cli/src/oclif/commands/users/get.js
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
const ZapierBaseCommand = require('../../ZapierBaseCommand');
|
||||
const { cyan, yellow } = require('colors/safe');
|
||||
const { listEndpoint } = require('../../../utils/api');
|
||||
const { buildFlags } = require('../../buildFlags');
|
||||
|
||||
class UsersListCommand extends ZapierBaseCommand {
|
||||
async perform() {
|
||||
this.startSpinner('Loading users');
|
||||
const { users } = await listEndpoint('invitees', 'users');
|
||||
|
||||
const cleanedUsers = users.map((u) => ({
|
||||
...u,
|
||||
app_version: u.app_version || 'All',
|
||||
}));
|
||||
|
||||
this.stopSpinner();
|
||||
|
||||
this.log(
|
||||
`\n${yellow(
|
||||
'Note',
|
||||
)} that this list of users is NOT a comprehensive list of everyone who is using your integration. It only includes users who were invited directly by email (using the \`users:add EMAIL\` command or the web UI).\n`,
|
||||
);
|
||||
|
||||
this.logTable({
|
||||
rows: cleanedUsers,
|
||||
headers: [
|
||||
['Email', 'email'],
|
||||
['Status', 'status'],
|
||||
['Version', 'app_version'],
|
||||
],
|
||||
emptyMessage: 'No users have been invited directly by email.',
|
||||
});
|
||||
|
||||
this.log(
|
||||
`\nTo invite users via a link, use the \`${cyan(
|
||||
'zapier-platform users:links',
|
||||
)}\` command. To invite a specific user by email, use the \`${cyan(
|
||||
'zapier-platform users:add',
|
||||
)}\` command.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UsersListCommand.flags = buildFlags({ opts: { format: true } });
|
||||
UsersListCommand.description = `Get a list of users who have been invited to your integration.
|
||||
|
||||
Note that this list of users is NOT a comprehensive list of everyone who is using your integration. It only includes users who were invited directly by email (using the \`${cyan(
|
||||
'zapier-platform users:add',
|
||||
)}\` command or the web UI). Users who joined by clicking links generated using the \`${cyan(
|
||||
'zapier-platform user:links',
|
||||
)}\` command won't show up here.`;
|
||||
UsersListCommand.aliases = ['users:list'];
|
||||
UsersListCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = UsersListCommand;
|
||||
45
vendor/zapier-platform/packages/cli/src/oclif/commands/users/links.js
vendored
Normal file
45
vendor/zapier-platform/packages/cli/src/oclif/commands/users/links.js
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
const ZapierBaseCommand = require('../../ZapierBaseCommand');
|
||||
const { bold, cyan } = require('colors/safe');
|
||||
const { listEndpoint } = require('../../../utils/api');
|
||||
const { buildFlags } = require('../../buildFlags');
|
||||
|
||||
class UsersLinksCommand extends ZapierBaseCommand {
|
||||
async perform() {
|
||||
this.startSpinner('Loading links');
|
||||
const { invite_url: inviteUrl, versions_invite_urls: versionInviteUrls } =
|
||||
await listEndpoint('invitees');
|
||||
|
||||
this.stopSpinner();
|
||||
|
||||
this.log(
|
||||
`\nYou can invite users to ${bold(
|
||||
'all',
|
||||
)} versions of your integration using the following link:`,
|
||||
);
|
||||
this.log(`\n${cyan(inviteUrl)}\n`);
|
||||
|
||||
this.log(
|
||||
'You can invite users to a specific integration version using the following links:',
|
||||
);
|
||||
this.logTable({
|
||||
rows: Object.entries(versionInviteUrls).map(([version, url]) => ({
|
||||
version,
|
||||
url,
|
||||
})),
|
||||
headers: [
|
||||
['Version', 'version'],
|
||||
['URL', 'url'],
|
||||
],
|
||||
});
|
||||
|
||||
this.log(
|
||||
'\nTo invite a specific user by email, use the `zapier-platform users:add` command.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UsersLinksCommand.flags = buildFlags({ opts: { format: true } });
|
||||
UsersLinksCommand.description = `Get a list of links that are used to invite users to your integration.`;
|
||||
UsersLinksCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = UsersLinksCommand;
|
||||
50
vendor/zapier-platform/packages/cli/src/oclif/commands/users/remove.js
vendored
Normal file
50
vendor/zapier-platform/packages/cli/src/oclif/commands/users/remove.js
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
const ZapierBaseCommand = require('../../ZapierBaseCommand');
|
||||
const { Args, Flags } = require('@oclif/core');
|
||||
const { cyan } = require('colors/safe');
|
||||
const { buildFlags } = require('../../buildFlags');
|
||||
const { callAPI } = require('../../../utils/api');
|
||||
|
||||
class UsersRemoveCommand extends ZapierBaseCommand {
|
||||
async perform() {
|
||||
if (
|
||||
!this.flags.force &&
|
||||
!(await this.confirm(
|
||||
`About to revoke access to ${cyan(
|
||||
this.args.email,
|
||||
)}. They won't be able to see your app in the editor and their Zaps will stop working. Are you sure?`,
|
||||
true,
|
||||
))
|
||||
) {
|
||||
this.log('\ncancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = await this.getWritableApp();
|
||||
this.startSpinner('Removing User');
|
||||
const url = `/apps/${id}/invitees/${this.args.email}`;
|
||||
await callAPI(url, { method: 'DELETE' });
|
||||
this.stopSpinner();
|
||||
}
|
||||
}
|
||||
|
||||
UsersRemoveCommand.args = {
|
||||
email: Args.string({
|
||||
description: 'The user to be removed.',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
UsersRemoveCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
force: Flags.boolean({
|
||||
char: 'f',
|
||||
description: 'Skips confirmation. Useful for running programatically.',
|
||||
}),
|
||||
},
|
||||
});
|
||||
UsersRemoveCommand.description = `Remove a user from all versions of your integration.
|
||||
|
||||
When this command is run, their Zaps will immediately turn off. They won't be able to use your app again until they're re-invited or it has gone public. In practice, this command isn't run often as it's very disruptive to users.`;
|
||||
UsersRemoveCommand.aliases = ['users:delete'];
|
||||
UsersRemoveCommand.skipValidInstallCheck = true;
|
||||
|
||||
module.exports = UsersRemoveCommand;
|
||||
156
vendor/zapier-platform/packages/cli/src/oclif/commands/validate.js
vendored
Normal file
156
vendor/zapier-platform/packages/cli/src/oclif/commands/validate.js
vendored
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
const colors = require('colors/safe');
|
||||
const { Flags } = require('@oclif/core');
|
||||
|
||||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
const { flattenCheckResult } = require('../../utils/display');
|
||||
const { localAppCommand } = require('../../utils/local');
|
||||
const { validateApp } = require('../../utils/api');
|
||||
const { maybeRunBuildScript } = require('../../utils/build');
|
||||
|
||||
class ValidateCommand extends BaseCommand {
|
||||
async perform() {
|
||||
if (!this.flags['skip-build']) {
|
||||
await maybeRunBuildScript({ printProgress: true });
|
||||
}
|
||||
|
||||
this.log('Validating project locally');
|
||||
|
||||
const errors = await localAppCommand({ command: 'validate' });
|
||||
const newErrors = errors.map((error) => ({
|
||||
...error,
|
||||
property: error.property.replace(/^instance/, 'App'),
|
||||
docLinks: (error.docLinks || []).join('\n'),
|
||||
}));
|
||||
this.logTable({
|
||||
rows: newErrors,
|
||||
headers: [
|
||||
['Property', 'property'],
|
||||
['Message', 'message'],
|
||||
['Links', 'docLinks'],
|
||||
],
|
||||
emptyMessage: 'No structural errors found during validation routine.',
|
||||
});
|
||||
|
||||
if (newErrors.length) {
|
||||
this.log(
|
||||
'Your integration is structurally invalid. Address concerns and run this command again.',
|
||||
);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
this.log('This project is structurally sound!');
|
||||
}
|
||||
|
||||
let checkResult = {};
|
||||
if (this.flags['without-style'] || process.exitCode === 1) {
|
||||
if (process.exitCode === 1) {
|
||||
this.log(
|
||||
colors.grey(
|
||||
'\nSkipping integration checks because schema did not validate.',
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
this.log();
|
||||
this.startSpinner('Running integration checks');
|
||||
|
||||
const rawDefinition = await localAppCommand({
|
||||
command: 'definition',
|
||||
});
|
||||
|
||||
checkResult = await validateApp(rawDefinition);
|
||||
}
|
||||
|
||||
const success = !checkResult.errors.total_failures;
|
||||
const message = 'Integration checks complete';
|
||||
this.stopSpinner({ success, message });
|
||||
|
||||
this.log(` - ${checkResult.passes.length} checks passed`);
|
||||
this.log(` - ${checkResult.errors.total_failures} checks failed`);
|
||||
this.log(
|
||||
` - ${checkResult.warnings.total_failures} checks with publishing warning`,
|
||||
);
|
||||
this.log(
|
||||
` - ${checkResult.suggestions.total_failures} checks with general warning`,
|
||||
);
|
||||
|
||||
const checkIssues = flattenCheckResult(checkResult);
|
||||
|
||||
this.log();
|
||||
if (checkIssues.length) {
|
||||
this.log('Here are the issues we found:');
|
||||
}
|
||||
|
||||
this.logTable({
|
||||
rows: checkIssues,
|
||||
headers: [
|
||||
['Category', 'category'],
|
||||
['Method', 'method'],
|
||||
['Description', 'description'],
|
||||
['Link', 'link'],
|
||||
],
|
||||
emptyMessage: 'Integration checks passed, no issues found.',
|
||||
});
|
||||
|
||||
const errorDisplay = checkResult.errors.display_label;
|
||||
const warningDisplay = checkResult.warnings.display_label;
|
||||
const suggestionDisplay = checkResult.suggestions.display_label;
|
||||
|
||||
if (checkIssues.length) {
|
||||
this.logTable({
|
||||
headers: [
|
||||
['', 'type'],
|
||||
['', 'description'],
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
type: `- ${colors.bold(errorDisplay)}`,
|
||||
description:
|
||||
'Issues that will prevent your integration from functioning properly. They block you from pushing.',
|
||||
},
|
||||
{
|
||||
type: `- ${colors.bold(warningDisplay)}`,
|
||||
description:
|
||||
'To-dos that must be addressed before your integration can be included in the App Directory. They block you from promoting and publishing.',
|
||||
},
|
||||
{
|
||||
type: `- ${colors.bold(suggestionDisplay)}`,
|
||||
description:
|
||||
"Issues and recommendations that need human reviews by Zapier before publishing your integration. They don't block.",
|
||||
},
|
||||
],
|
||||
hasBorder: false,
|
||||
showHeaders: false,
|
||||
style: { head: [], 'padding-left': 0, 'padding-right': 0 },
|
||||
});
|
||||
}
|
||||
this.log();
|
||||
}
|
||||
}
|
||||
|
||||
ValidateCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
'without-style': Flags.boolean({
|
||||
description: 'Forgo pinging the Zapier server to run further checks.',
|
||||
}),
|
||||
'skip-build': Flags.boolean({
|
||||
description: 'Skip running the _zapier-build script before validation.',
|
||||
}),
|
||||
},
|
||||
opts: {
|
||||
format: true,
|
||||
},
|
||||
});
|
||||
|
||||
ValidateCommand.examples = [
|
||||
'zapier-platform validate',
|
||||
'zapier-platform validate --without-style',
|
||||
'zapier-platform validate --skip-build',
|
||||
'zapier-platform validate --format json',
|
||||
];
|
||||
ValidateCommand.description = `Validate your integration.
|
||||
|
||||
Run the standard validation routine powered by json-schema that checks your integration for any structural errors. This is the same routine that runs during \`zapier-platform build\`, \`zapier-platform upload\`, \`zapier-platform push\` or even as a test in \`zapier-platform test\`.`;
|
||||
|
||||
module.exports = ValidateCommand;
|
||||
57
vendor/zapier-platform/packages/cli/src/oclif/commands/versions.js
vendored
Normal file
57
vendor/zapier-platform/packages/cli/src/oclif/commands/versions.js
vendored
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
const { Flags } = require('@oclif/core');
|
||||
const BaseCommand = require('../ZapierBaseCommand');
|
||||
const { buildFlags } = require('../buildFlags');
|
||||
|
||||
const { listVersions } = require('../../utils/api');
|
||||
|
||||
class VersionsCommand extends BaseCommand {
|
||||
async perform() {
|
||||
this.startSpinner('Loading versions');
|
||||
const { versions } = await listVersions();
|
||||
this.stopSpinner();
|
||||
const rows = versions.map((v) => ({
|
||||
...v,
|
||||
state: v.lifecycle.status,
|
||||
}));
|
||||
|
||||
const visibleVersions = this.flags.all
|
||||
? rows
|
||||
: rows.filter((v) => v.state !== 'deprecated');
|
||||
|
||||
this.logTable({
|
||||
rows: visibleVersions,
|
||||
headers: [
|
||||
['Version', 'version'],
|
||||
['Platform', 'platform_version'],
|
||||
['Zap Users', 'user_count'],
|
||||
['State', 'state'],
|
||||
['Legacy Date', 'legacy_date'],
|
||||
['Deprecation Date', 'deprecation_date'],
|
||||
['Created at', 'date'],
|
||||
['Updated at', 'last_changed'],
|
||||
],
|
||||
emptyMessage:
|
||||
'No versions to show. Try adding one with the `zapier-platform push` command',
|
||||
});
|
||||
|
||||
if (versions.map((v) => v.user_count).filter((c) => c === null).length) {
|
||||
this.warn(
|
||||
'Some user counts are still being calculated - run this command again in ~10 seconds (or longer if your integration has lots of users).',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VersionsCommand.skipValidInstallCheck = true;
|
||||
VersionsCommand.flags = buildFlags({
|
||||
commandFlags: {
|
||||
all: Flags.boolean({
|
||||
char: 'a',
|
||||
description: `List all versions, including deprecated versions.`,
|
||||
}),
|
||||
},
|
||||
opts: { format: true },
|
||||
});
|
||||
VersionsCommand.description = `List the versions of your integration available for use in Zapier automations.`;
|
||||
|
||||
module.exports = VersionsCommand;
|
||||
11
vendor/zapier-platform/packages/cli/src/oclif/hooks/checkValidNodeVersion.js
vendored
Normal file
11
vendor/zapier-platform/packages/cli/src/oclif/hooks/checkValidNodeVersion.js
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
const { isValidNodeVersion } = require('../../utils/misc');
|
||||
const { LAMBDA_VERSION } = require('../../constants');
|
||||
|
||||
// can't be fat arrow because it inherits `this` from commands
|
||||
module.exports = function () {
|
||||
if (!isValidNodeVersion()) {
|
||||
this.error(
|
||||
`Requires node version >= ${LAMBDA_VERSION}, found ${process.versions.node}. Please upgrade Node.js.`,
|
||||
);
|
||||
}
|
||||
};
|
||||
18
vendor/zapier-platform/packages/cli/src/oclif/hooks/deprecated.js
vendored
Normal file
18
vendor/zapier-platform/packages/cli/src/oclif/hooks/deprecated.js
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// this is an init hook because the info about which command is clobbered by the time we get to the pre_run hook
|
||||
|
||||
// deprecated => recommended
|
||||
const deprecatedCommands = {
|
||||
apps: 'integrations',
|
||||
};
|
||||
|
||||
// can't be fat arrow because it inherits `this` from commands
|
||||
module.exports = function (options) {
|
||||
if (deprecatedCommands[options.id]) {
|
||||
this.warn(
|
||||
`The \`${options.id}\` command is deprecated. Use the \`${
|
||||
deprecatedCommands[options.id]
|
||||
}\` command instead.`,
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
};
|
||||
45
vendor/zapier-platform/packages/cli/src/oclif/hooks/getAppRegistrationFieldChoices.js
vendored
Normal file
45
vendor/zapier-platform/packages/cli/src/oclif/hooks/getAppRegistrationFieldChoices.js
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
const { callAPI } = require('../../utils/api');
|
||||
|
||||
module.exports = async function (options) {
|
||||
// We only need to run this for the register command
|
||||
if (!options || !options.id || options.id !== 'register') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const enumFieldChoices = {};
|
||||
let formFields;
|
||||
|
||||
try {
|
||||
formFields = await callAPI('/apps/fields-choices', { skipDeployKey: true });
|
||||
} catch (e) {
|
||||
this.error(
|
||||
`Unable to connect to Zapier API. Please check your connection and try again. ${e}`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const fieldName of ['intention', 'role', 'app_category']) {
|
||||
enumFieldChoices[fieldName] = formFields[fieldName];
|
||||
}
|
||||
|
||||
this.config.enumFieldChoices = enumFieldChoices;
|
||||
|
||||
// This enables us to see all available options when running `zapier-platform register --help`
|
||||
const cmd = options.config.findCommand('register');
|
||||
if (cmd && cmd.flags) {
|
||||
if (cmd.flags.audience) {
|
||||
cmd.flags.audience.options = formFields.intention.map(
|
||||
(audienceOption) => audienceOption.value,
|
||||
);
|
||||
}
|
||||
if (cmd.flags.role) {
|
||||
cmd.flags.role.options = formFields.role.map(
|
||||
(roleOption) => roleOption.value,
|
||||
);
|
||||
}
|
||||
if (cmd.flags.category) {
|
||||
cmd.flags.category.options = formFields.app_category.map(
|
||||
(categoryOption) => categoryOption.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
23
vendor/zapier-platform/packages/cli/src/oclif/hooks/renderMarkdownHelp.js
vendored
Normal file
23
vendor/zapier-platform/packages/cli/src/oclif/hooks/renderMarkdownHelp.js
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
const chalk = require('chalk');
|
||||
const { marked } = require('marked');
|
||||
const { markedTerminal } = require('marked-terminal');
|
||||
|
||||
marked.use(
|
||||
markedTerminal({
|
||||
tab: 2,
|
||||
width: process.stdout.getWindowSize()[0] - 2,
|
||||
reflowText: true,
|
||||
codespan: chalk.underline.bold,
|
||||
}),
|
||||
);
|
||||
|
||||
module.exports = (options) => {
|
||||
const cmdId = options.id === 'help' ? options.argv[0] : options.id;
|
||||
const cmd = options.config.findCommand(cmdId);
|
||||
if (cmd) {
|
||||
if (cmd.description) {
|
||||
cmd.description = marked.parse(cmd.description).trim();
|
||||
}
|
||||
// TODO: Do the same for flag descriptions?
|
||||
}
|
||||
};
|
||||
15
vendor/zapier-platform/packages/cli/src/oclif/hooks/updateNotifier.js
vendored
Normal file
15
vendor/zapier-platform/packages/cli/src/oclif/hooks/updateNotifier.js
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
const { createUpdateNotifier } = require('../../utils/esm-wrapper');
|
||||
const pkg = require('../../../package.json');
|
||||
const { UPDATE_NOTIFICATION_INTERVAL } = require('../../constants');
|
||||
|
||||
// can't be fat arrow because it inherits `this` from commands
|
||||
// Made async because createUpdateNotifier() uses dynamic import() to load ESM-only update-notifier package
|
||||
module.exports = async function (options) {
|
||||
const notifier = await createUpdateNotifier({
|
||||
// await needed for ESM dynamic import
|
||||
pkg,
|
||||
updateCheckInterval: UPDATE_NOTIFICATION_INTERVAL,
|
||||
});
|
||||
|
||||
notifier.notify({ isGlobal: true });
|
||||
};
|
||||
38
vendor/zapier-platform/packages/cli/src/oclif/hooks/versionInfo.js
vendored
Normal file
38
vendor/zapier-platform/packages/cli/src/oclif/hooks/versionInfo.js
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// customize the output of `zapier --version`
|
||||
// see: https://github.com/oclif/oclif/issues/254#issuecomment-591433963
|
||||
|
||||
const { get } = require('lodash');
|
||||
const { PLATFORM_PACKAGE } = require('../../constants');
|
||||
const path = require('path');
|
||||
|
||||
const VERSION_ARGS = ['version', '-v', '--version', '-V'];
|
||||
|
||||
module.exports = (options) => {
|
||||
const firstArg = options.id;
|
||||
if (!VERSION_ARGS.includes(firstArg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
[
|
||||
`* CLI version: ${options.config.version}`,
|
||||
`* Node.js version: ${process.version}`,
|
||||
`* OS info: ${options.config.platform}-${options.config.arch}`,
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
try {
|
||||
const pJson = require(path.join(process.cwd(), 'package.json'));
|
||||
|
||||
// are we in an app directory?
|
||||
const maybeCoreDepVersion = get(pJson, ['dependencies', PLATFORM_PACKAGE]);
|
||||
if (maybeCoreDepVersion) {
|
||||
console.log(
|
||||
`* \`${PLATFORM_PACKAGE}\` dependency: ${maybeCoreDepVersion}`,
|
||||
);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// very important to exit, this will eventually fail to find a command
|
||||
process.exit(0);
|
||||
};
|
||||
56
vendor/zapier-platform/packages/cli/src/oclif/oCommands.js
vendored
Normal file
56
vendor/zapier-platform/packages/cli/src/oclif/oCommands.js
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// This list tells oclif which commands we want to expose.
|
||||
// See the oclif.commands setting in packages/cli/package.json.
|
||||
// The build-docs script (packages/cli/scripts/docs.js) also uses this list
|
||||
// to generate the command reference documentation.
|
||||
const COMMANDS = {
|
||||
analytics: require('./commands/analytics'),
|
||||
apps: true,
|
||||
build: require('./commands/build'),
|
||||
canary: true,
|
||||
'canary:create': require('./commands/canary/create'),
|
||||
'canary:delete': require('./commands/canary/delete'),
|
||||
'canary:list': require('./commands/canary/list'),
|
||||
cache: true,
|
||||
'cache:clear': require('./commands/cache/clear'),
|
||||
convert: require('./commands/convert'),
|
||||
deprecate: require('./commands/deprecate'),
|
||||
delete: true,
|
||||
'delete:integration': require('./commands/delete/integration'),
|
||||
'delete:version': require('./commands/delete/version'),
|
||||
describe: require('./commands/describe'),
|
||||
env: true, // used so that aliases are properly routed into oclif, but `env` itself doesn't show in help/docs
|
||||
'env:get': require('./commands/env/get'),
|
||||
'env:set': require('./commands/env/set'),
|
||||
'env:unset': require('./commands/env/unset'),
|
||||
history: require('./commands/history'),
|
||||
jobs: require('./commands/jobs'),
|
||||
init: require('./commands/init'),
|
||||
integrations: require('./commands/integrations'),
|
||||
invoke: require('./commands/invoke'),
|
||||
link: require('./commands/link'),
|
||||
legacy: require('./commands/legacy'),
|
||||
login: require('./commands/login'),
|
||||
logs: require('./commands/logs'),
|
||||
logout: require('./commands/logout'),
|
||||
migrate: require('./commands/migrate'),
|
||||
promote: require('./commands/promote'),
|
||||
pull: require('./commands/pull'),
|
||||
push: require('./commands/push'),
|
||||
scaffold: require('./commands/scaffold'),
|
||||
register: require('./commands/register'),
|
||||
team: true,
|
||||
'team:add': require('./commands/team/add'),
|
||||
'team:get': require('./commands/team/get'),
|
||||
'team:remove': require('./commands/team/remove'),
|
||||
test: require('./commands/test'),
|
||||
upload: require('./commands/upload'),
|
||||
users: true,
|
||||
'users:add': require('./commands/users/add'),
|
||||
'users:get': require('./commands/users/get'),
|
||||
'users:links': require('./commands/users/links'),
|
||||
'users:remove': require('./commands/users/remove'),
|
||||
validate: require('./commands/validate'),
|
||||
versions: require('./commands/versions'),
|
||||
};
|
||||
|
||||
module.exports = { COMMANDS };
|
||||
Loading…
Add table
Add a link
Reference in a new issue