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
36
vendor/zapier-platform/packages/cli/src/app-templates.js
vendored
Normal file
36
vendor/zapier-platform/packages/cli/src/app-templates.js
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// list of template (example) apps
|
||||
// https://github.com/zapier?utf8=%E2%9C%93&q=zapier-platform-example-app-&type=&language=
|
||||
module.exports = [
|
||||
// basic
|
||||
'minimal',
|
||||
'trigger',
|
||||
'search',
|
||||
'create',
|
||||
|
||||
// auth types
|
||||
'basic-auth',
|
||||
'custom-auth',
|
||||
'digest-auth',
|
||||
'oauth2',
|
||||
'oauth1-trello',
|
||||
'oauth1-tumblr',
|
||||
'oauth1-twitter',
|
||||
'session-auth',
|
||||
|
||||
// features
|
||||
'dynamic-dropdown',
|
||||
'files',
|
||||
'line-items',
|
||||
'middleware',
|
||||
'resource',
|
||||
'rest-hooks',
|
||||
'search-or-create',
|
||||
|
||||
// transpilers
|
||||
'babel',
|
||||
'typescript',
|
||||
|
||||
// full examples
|
||||
'github',
|
||||
'onedrive',
|
||||
];
|
||||
6
vendor/zapier-platform/packages/cli/src/bin/run
vendored
Executable file
6
vendor/zapier-platform/packages/cli/src/bin/run
vendored
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
(async () => {
|
||||
const oclif = await import('@oclif/core');
|
||||
await oclif.execute({ development: false, dir: __dirname });
|
||||
})();
|
||||
109
vendor/zapier-platform/packages/cli/src/constants.js
vendored
Normal file
109
vendor/zapier-platform/packages/cli/src/constants.js
vendored
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const versionStore = require('./version-store');
|
||||
|
||||
const BASE_ENDPOINT = process.env.ZAPIER_BASE_ENDPOINT || 'https://zapier.com';
|
||||
const API_PATH = '/api/platform/cli';
|
||||
const ENDPOINT = process.env.ZAPIER_ENDPOINT || BASE_ENDPOINT + API_PATH;
|
||||
const STARTER_REPO =
|
||||
process.env.ZAPIER_STARTER_REPO || 'zapier/zapier-platform-example-app';
|
||||
const AUTH_LOCATION_RAW = '~/.zapierrc';
|
||||
const AUTH_LOCATION =
|
||||
process.env.ZAPIER_AUTH_LOCATION || path.resolve(os.homedir(), '.zapierrc');
|
||||
const CURRENT_APP_FILE = process.env.ZAPIER_CURRENT_APP_FILE || '.zapierapprc';
|
||||
const PLATFORM_PACKAGE = 'zapier-platform-core';
|
||||
const LEGACY_RUNNER_PACKAGE = 'zapier-platform-legacy-scripting-runner';
|
||||
const BUILD_DIR = 'build';
|
||||
const DEFINITION_PATH = `${BUILD_DIR}/definition.json`;
|
||||
const BUILD_PATH = `${BUILD_DIR}/build.zip`;
|
||||
const SOURCE_PATH = `${BUILD_DIR}/source.zip`;
|
||||
const NODE_VERSION = versionStore[versionStore.length - 1].nodeVersion;
|
||||
const LAMBDA_VERSION = `v${NODE_VERSION}`;
|
||||
const NODE_VERSION_CLI_REQUIRES = '>=22'; // should be the oldest non-ETL version
|
||||
const AUTH_KEY = 'deployKey';
|
||||
const ANALYTICS_KEY = 'analyticsMode';
|
||||
const ANALYTICS_MODES = {
|
||||
enabled: 'enabled',
|
||||
anonymous: 'anonymous',
|
||||
disabled: 'disabled',
|
||||
};
|
||||
|
||||
const packageJson = require('../package.json');
|
||||
const PACKAGE_NAME = packageJson.name;
|
||||
const PACKAGE_VERSION = packageJson.version;
|
||||
|
||||
const UPDATE_NOTIFICATION_INTERVAL = 1000 * 60 * 60 * 24 * 7; // one week
|
||||
|
||||
const CHECK_REF_DOC_LINK =
|
||||
'https://docs.zapier.com/platform/publish/integration-checks-reference';
|
||||
|
||||
const ISSUES_URL =
|
||||
'https://github.com/zapier/zapier-platform/issues/new/choose';
|
||||
|
||||
// can't just read from argv because they could have lots of extra data, such as
|
||||
// [ '/Users/david/.nvm/versions/node/v10.13.0/bin/node',
|
||||
// '/Users/david/projects/zapier/platform/node_modules/.bin/mocha',
|
||||
// 'src/tests' ]
|
||||
const argvStr = process.argv.join(' ');
|
||||
const IS_TESTING =
|
||||
argvStr.includes('mocha') ||
|
||||
argvStr.includes('jest') ||
|
||||
(process.env.NODE_ENV || '').toLowerCase().startsWith('test');
|
||||
|
||||
const MIN_TITLE_LENGTH = 2;
|
||||
const MAX_DESCRIPTION_LENGTH = 140;
|
||||
|
||||
const EXAMPLE_CHANGELOG = `
|
||||
## 3.0.0
|
||||
|
||||
Made some changes that affect app actions
|
||||
|
||||
1. Update the trigger/pr_review action, as well as changes for #456
|
||||
2. Fix trigger/new_card #208
|
||||
3. New action! create/add_contact
|
||||
|
||||
However, we also addressed fixed open issues!
|
||||
|
||||
- Fix #123 and an issue with create/send_message
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* Fix some bugs.
|
||||
* Major docs fixes.
|
||||
|
||||
## 1.0.0
|
||||
|
||||
Initial release to public.
|
||||
`;
|
||||
|
||||
module.exports = {
|
||||
ANALYTICS_KEY,
|
||||
ANALYTICS_MODES,
|
||||
API_PATH,
|
||||
AUTH_KEY,
|
||||
AUTH_LOCATION,
|
||||
AUTH_LOCATION_RAW,
|
||||
BASE_ENDPOINT,
|
||||
BUILD_DIR,
|
||||
BUILD_PATH,
|
||||
CHECK_REF_DOC_LINK,
|
||||
CURRENT_APP_FILE,
|
||||
DEFINITION_PATH,
|
||||
ENDPOINT,
|
||||
IS_TESTING,
|
||||
ISSUES_URL,
|
||||
LAMBDA_VERSION,
|
||||
LEGACY_RUNNER_PACKAGE,
|
||||
MIN_TITLE_LENGTH,
|
||||
MAX_DESCRIPTION_LENGTH,
|
||||
NODE_VERSION,
|
||||
NODE_VERSION_CLI_REQUIRES,
|
||||
PACKAGE_NAME,
|
||||
PACKAGE_VERSION,
|
||||
PLATFORM_PACKAGE,
|
||||
SOURCE_PATH,
|
||||
STARTER_REPO,
|
||||
UPDATE_NOTIFICATION_INTERVAL,
|
||||
EXAMPLE_CHANGELOG,
|
||||
};
|
||||
346
vendor/zapier-platform/packages/cli/src/generators/index.js
vendored
Normal file
346
vendor/zapier-platform/packages/cli/src/generators/index.js
vendored
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
const path = require('path');
|
||||
|
||||
const { merge } = require('lodash');
|
||||
const filter = require('gulp-filter');
|
||||
const { createGeneratorClass } = require('../utils/esm-wrapper');
|
||||
const prettier = require('gulp-prettier');
|
||||
|
||||
const { PACKAGE_VERSION, PLATFORM_PACKAGE } = require('../constants');
|
||||
const authFilesCodegen = require('../utils/auth-files-codegen');
|
||||
const PullGeneratorPromise = require('./pull');
|
||||
|
||||
const writeGenericReadme = (gen) => {
|
||||
gen.fs.copyTpl(
|
||||
gen.templatePath('README.template.md'),
|
||||
gen.destinationPath('README.md'),
|
||||
{ name: gen.options.packageName },
|
||||
);
|
||||
};
|
||||
|
||||
const appendReadme = (gen) => {
|
||||
const content = gen.fs.read(
|
||||
gen.templatePath(gen.options.template, 'README.md'),
|
||||
{ defaults: '' },
|
||||
);
|
||||
if (content) {
|
||||
gen.fs.append(gen.destinationPath('README.md'), '\n' + content);
|
||||
}
|
||||
};
|
||||
|
||||
const writeGitignore = (gen) => {
|
||||
gen.fs.copy(gen.templatePath('gitignore'), gen.destinationPath('.gitignore'));
|
||||
};
|
||||
|
||||
const writeGenericPackageJson = (gen, packageJsonExtension) => {
|
||||
const moduleExtension =
|
||||
gen.options.module === 'esm'
|
||||
? {
|
||||
exports: './index.js',
|
||||
type: 'module',
|
||||
}
|
||||
: {
|
||||
main: 'index.js',
|
||||
};
|
||||
|
||||
const fullExtension = merge(moduleExtension, packageJsonExtension);
|
||||
|
||||
gen.fs.writeJSON(
|
||||
gen.destinationPath('package.json'),
|
||||
merge(
|
||||
{
|
||||
name: gen.options.packageName,
|
||||
version: '1.0.0',
|
||||
description: '',
|
||||
scripts: {
|
||||
test: 'jest --testTimeout 10000',
|
||||
},
|
||||
dependencies: {
|
||||
[PLATFORM_PACKAGE]: PACKAGE_VERSION,
|
||||
},
|
||||
devDependencies: {
|
||||
jest: '^29.6.0',
|
||||
},
|
||||
private: true,
|
||||
},
|
||||
fullExtension,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const writeGenericTypeScriptPackageJson = (gen, packageJsonExtension) => {
|
||||
gen.fs.writeJSON(
|
||||
gen.destinationPath('package.json'),
|
||||
merge(
|
||||
{
|
||||
name: gen.options.packageName,
|
||||
version: '1.0.0',
|
||||
description: '',
|
||||
scripts: {
|
||||
test: 'npm run build && vitest --run',
|
||||
clean: 'rimraf ./dist ./build',
|
||||
build: 'npm run clean && tsc',
|
||||
dev: 'npm run build -- --watch',
|
||||
'_zapier-build': 'npm run build',
|
||||
},
|
||||
dependencies: {
|
||||
[PLATFORM_PACKAGE]: PACKAGE_VERSION,
|
||||
},
|
||||
devDependencies: {
|
||||
rimraf: '^5.0.10',
|
||||
typescript: '5.6.2',
|
||||
vitest: '^2.1.2',
|
||||
},
|
||||
private: true,
|
||||
exports: './dist/index.js',
|
||||
type: 'module',
|
||||
},
|
||||
packageJsonExtension,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const writeGenericIndex = (gen, hasAuth) => {
|
||||
const templatePath =
|
||||
gen.options.module === 'esm'
|
||||
? 'index-esm.template.js'
|
||||
: 'index.template.js';
|
||||
gen.fs.copyTpl(
|
||||
gen.templatePath(templatePath),
|
||||
gen.destinationPath('index.js'),
|
||||
{ corePackageName: PLATFORM_PACKAGE, hasAuth },
|
||||
);
|
||||
};
|
||||
|
||||
const writeGenericTypescriptIndex = (gen) => {
|
||||
gen.fs.copyTpl(
|
||||
gen.templatePath('index.template.ts'),
|
||||
gen.destinationPath('src/index.ts'),
|
||||
{ corePackageName: PLATFORM_PACKAGE },
|
||||
);
|
||||
};
|
||||
|
||||
const authTypes = {
|
||||
'basic-auth': 'basic',
|
||||
'custom-auth': 'custom',
|
||||
'digest-auth': 'digest',
|
||||
'oauth1-trello': 'oauth1',
|
||||
oauth2: 'oauth2',
|
||||
'session-auth': 'session',
|
||||
};
|
||||
|
||||
const writeGenericAuth = (gen) => {
|
||||
const authType = authTypes[gen.options.template];
|
||||
const content = authFilesCodegen[authType](gen.options.language);
|
||||
const destPath = (key) =>
|
||||
gen.options.language === 'typescript' ? `src/${key}.ts` : `${key}.js`;
|
||||
|
||||
Object.entries(content).forEach(([key, value]) => {
|
||||
gen.fs.write(gen.destinationPath(destPath(key)), value);
|
||||
});
|
||||
};
|
||||
|
||||
const writeGenericAuthTest = (gen) => {
|
||||
const authType = authTypes[gen.options.template];
|
||||
const fileExtension = gen.options.language === 'typescript' ? 'ts' : 'js';
|
||||
const destPath = gen.options.language === 'typescript' ? 'src/test' : 'test';
|
||||
gen.fs.copyTpl(
|
||||
gen.templatePath(
|
||||
`authTests/${authType || 'generic'}.test.${fileExtension}`,
|
||||
),
|
||||
gen.destinationPath(`${destPath}/authentication.test.${fileExtension}`),
|
||||
);
|
||||
};
|
||||
|
||||
const writeGenericTest = (gen) => {
|
||||
gen.fs.copyTpl(
|
||||
gen.templatePath('authTests/generic.test.js'),
|
||||
gen.destinationPath('test/example.test.js'),
|
||||
);
|
||||
};
|
||||
|
||||
// Write files for templates that demonstrate an auth type
|
||||
const writeForAuthTemplate = (gen) => {
|
||||
writeGitignore(gen);
|
||||
writeGenericReadme(gen);
|
||||
if (gen.options.language === 'typescript') {
|
||||
writeGenericTypescriptIndex(gen);
|
||||
writeGenericTypeScriptPackageJson(gen);
|
||||
gen.fs.copyTpl(
|
||||
gen.templatePath('tsconfig.template.json'),
|
||||
gen.destinationPath('tsconfig.json'),
|
||||
);
|
||||
} else {
|
||||
writeGenericIndex(gen, true);
|
||||
writeGenericPackageJson(gen);
|
||||
}
|
||||
writeGenericAuth(gen);
|
||||
writeGenericAuthTest(gen);
|
||||
};
|
||||
|
||||
const writeForMinimalTemplate = (gen) => {
|
||||
writeGitignore(gen);
|
||||
writeGenericReadme(gen);
|
||||
writeGenericPackageJson(gen);
|
||||
writeGenericIndex(gen, false);
|
||||
writeGenericTest(gen);
|
||||
};
|
||||
|
||||
// Write files for "standalone" templates, which essentially just copies an
|
||||
// example directory
|
||||
const writeForStandaloneTemplate = (gen) => {
|
||||
writeGitignore(gen);
|
||||
writeGenericReadme(gen);
|
||||
appendReadme(gen);
|
||||
|
||||
const packageJsonExtension = {
|
||||
// Put template-specific package.json settings here, grouped by template
|
||||
// names. This is going to used to extend the generic package.json.
|
||||
files: {
|
||||
dependencies: {
|
||||
'form-data': '4.0.0',
|
||||
},
|
||||
},
|
||||
}[gen.options.template];
|
||||
|
||||
writeGenericPackageJson(gen, packageJsonExtension);
|
||||
|
||||
gen.fs.copy(
|
||||
gen.templatePath(gen.options.template, '**', '*.{js,json,ts}'),
|
||||
gen.destinationPath(),
|
||||
);
|
||||
};
|
||||
|
||||
const TEMPLATE_ROUTES = {
|
||||
'basic-auth': writeForAuthTemplate,
|
||||
callback: writeForStandaloneTemplate,
|
||||
'custom-auth': writeForAuthTemplate,
|
||||
'digest-auth': writeForAuthTemplate,
|
||||
'dynamic-dropdown': writeForStandaloneTemplate,
|
||||
files: writeForStandaloneTemplate,
|
||||
'line-items': writeForStandaloneTemplate,
|
||||
minimal: writeForMinimalTemplate,
|
||||
'oauth1-trello': writeForAuthTemplate,
|
||||
oauth2: writeForAuthTemplate,
|
||||
openai: writeForStandaloneTemplate,
|
||||
'search-or-create': writeForStandaloneTemplate,
|
||||
'session-auth': writeForAuthTemplate,
|
||||
};
|
||||
|
||||
const ESM_SUPPORTED_TEMPLATES = ['minimal'];
|
||||
|
||||
// Which templates can be used with the --language typescript flag
|
||||
const TS_SUPPORTED_TEMPLATES = [
|
||||
'basic-auth',
|
||||
'custom-auth',
|
||||
'digest-auth',
|
||||
'oauth1-trello',
|
||||
'oauth2',
|
||||
'session-auth',
|
||||
];
|
||||
|
||||
const TEMPLATE_CHOICES = Object.keys(TEMPLATE_ROUTES);
|
||||
|
||||
const ProjectGeneratorPromise = createGeneratorClass((Generator) => {
|
||||
return class ProjectGenerator extends Generator {
|
||||
initializing() {
|
||||
this.sourceRoot(path.resolve(__dirname, 'templates'));
|
||||
this.destinationRoot(path.resolve(this.options.path));
|
||||
|
||||
const jsFilter = filter(['*.js', '*.json', '*.ts'], { restore: true });
|
||||
this.queueTransformStream(
|
||||
{ disabled: true },
|
||||
jsFilter,
|
||||
prettier({ singleQuote: true }),
|
||||
jsFilter.restore,
|
||||
);
|
||||
}
|
||||
|
||||
async prompting() {
|
||||
if (!this.options.template) {
|
||||
// Filter template choices based on language and module type
|
||||
let templateChoices = TEMPLATE_CHOICES;
|
||||
let defaultTemplate = 'minimal';
|
||||
|
||||
// TypeScript filtering takes precedence over ESM filtering
|
||||
if (this.options.language === 'typescript') {
|
||||
templateChoices = TS_SUPPORTED_TEMPLATES;
|
||||
defaultTemplate = 'basic-auth';
|
||||
} else if (this.options.module === 'esm') {
|
||||
templateChoices = ESM_SUPPORTED_TEMPLATES;
|
||||
defaultTemplate = 'minimal'; // minimal is the only ESM template
|
||||
}
|
||||
|
||||
this.answers = await this.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'template',
|
||||
choices: templateChoices,
|
||||
message: 'Choose a project template to start with:',
|
||||
default: defaultTemplate,
|
||||
},
|
||||
]);
|
||||
this.options.template = this.answers.template;
|
||||
}
|
||||
|
||||
if (
|
||||
ESM_SUPPORTED_TEMPLATES.includes(this.options.template) &&
|
||||
!this.options.module
|
||||
) {
|
||||
this.answers = await this.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'module',
|
||||
choices: ['esm', 'commonjs'],
|
||||
message: 'Choose module type:',
|
||||
default: 'esm',
|
||||
},
|
||||
]);
|
||||
this.options.module = this.answers.module;
|
||||
}
|
||||
|
||||
if (this.options.language) {
|
||||
if (this.options.language === 'typescript') {
|
||||
// check if the template supports typescript
|
||||
if (!TS_SUPPORTED_TEMPLATES.includes(this.options.template)) {
|
||||
throw new Error(
|
||||
'Typescript is not supported for this template, please use a different template or set the language to javascript. Supported templates: ' +
|
||||
TS_SUPPORTED_TEMPLATES.join(', '),
|
||||
);
|
||||
}
|
||||
// if they try to combine typescript with commonjs, throw an error
|
||||
if (this.options.module === 'commonjs') {
|
||||
throw new Error('Typescript is not supported for commonjs');
|
||||
} // esm is supported for typescript templates
|
||||
}
|
||||
} else {
|
||||
// default to javascript for the language if it's not set
|
||||
this.options.language = 'javascript';
|
||||
}
|
||||
|
||||
if (
|
||||
!ESM_SUPPORTED_TEMPLATES.includes(this.options.template) &&
|
||||
this.options.module === 'esm' &&
|
||||
this.options.language === 'javascript'
|
||||
) {
|
||||
throw new Error(
|
||||
'ESM is not supported for this template, please use a different template, set the module to commonjs, or try setting the language to Typescript',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
writing() {
|
||||
this.options.packageName = path.basename(this.options.path);
|
||||
|
||||
const writeFunc = TEMPLATE_ROUTES[this.options.template];
|
||||
writeFunc(this);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
TEMPLATE_CHOICES,
|
||||
ESM_SUPPORTED_TEMPLATES,
|
||||
TS_SUPPORTED_TEMPLATES,
|
||||
PullGenerator: PullGeneratorPromise,
|
||||
ProjectGenerator: ProjectGeneratorPromise,
|
||||
};
|
||||
55
vendor/zapier-platform/packages/cli/src/generators/pull.js
vendored
Normal file
55
vendor/zapier-platform/packages/cli/src/generators/pull.js
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
const colors = require('colors/safe');
|
||||
const debug = require('debug')('zapier:pull');
|
||||
const inquirer = require('inquirer');
|
||||
const path = require('path');
|
||||
const { createGeneratorClass } = require('../utils/esm-wrapper');
|
||||
|
||||
const maybeOverwriteFiles = async (gen) => {
|
||||
const dstDir = gen.options.dstDir;
|
||||
const srcDir = gen.options.srcDir;
|
||||
for (const file of gen.options.sourceFiles) {
|
||||
gen.fs.copy(path.join(srcDir, file), path.join(dstDir, file), gen.options);
|
||||
}
|
||||
};
|
||||
|
||||
// Export a factory function that creates the PullGenerator class
|
||||
module.exports = createGeneratorClass((Generator) => {
|
||||
return class PullGenerator extends Generator {
|
||||
initializing() {
|
||||
debug('SRC', this.options.sourceFiles);
|
||||
}
|
||||
|
||||
prompting() {
|
||||
const prompts = [
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: `Warning: You are about to overwrite existing files.
|
||||
|
||||
Before proceeding, please make sure you have saved your work. Consider creating a backup or saving your current state in a git branch.
|
||||
|
||||
If presented with a series of options ('ynarxdeiH'), you may
|
||||
press Enter to view more details about each option. For example, 'x' will abort the process.
|
||||
|
||||
Do you want to continue?`,
|
||||
default: false,
|
||||
},
|
||||
];
|
||||
|
||||
return inquirer.prompt(prompts).then((answers) => {
|
||||
if (!answers.confirm) {
|
||||
this.log(colors.green('zapier pull cancelled'));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
writing() {
|
||||
maybeOverwriteFiles(this);
|
||||
}
|
||||
|
||||
end() {
|
||||
this.log(colors.green('zapier pull completed successfully'));
|
||||
}
|
||||
};
|
||||
});
|
||||
34
vendor/zapier-platform/packages/cli/src/generators/templates/README.template.md
vendored
Normal file
34
vendor/zapier-platform/packages/cli/src/generators/templates/README.template.md
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# <%= name %>
|
||||
|
||||
This Zapier integration project is generated by the `zapier-platform init` CLI command.
|
||||
|
||||
These are what you normally do next:
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install --ignore-scripts # or you can use pnpm or yarn
|
||||
|
||||
# Run tests
|
||||
zapier-platform test
|
||||
|
||||
# Register the integration on Zapier if you haven't
|
||||
zapier-platform register "App Title"
|
||||
|
||||
# Or you can link to an existing integration on Zapier
|
||||
zapier-platform link
|
||||
|
||||
# Push it to Zapier
|
||||
zapier-platform push
|
||||
```
|
||||
|
||||
Then, to add more features, you can use the `zapier-platform scaffold` command, for example:
|
||||
|
||||
```bash
|
||||
# Add a trigger
|
||||
zapier-platform scaffold trigger contact
|
||||
|
||||
# Add an action
|
||||
zapier-platform scaffold create contact
|
||||
```
|
||||
|
||||
Find out more on the latest docs: https://docs.zapier.com/platform
|
||||
43
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/basic.test.js
vendored
Normal file
43
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/basic.test.js
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/* globals describe, it, expect */
|
||||
|
||||
const zapier = require('zapier-platform-core');
|
||||
|
||||
const App = require('../index');
|
||||
const appTester = zapier.createAppTester(App);
|
||||
|
||||
describe('basic auth', () => {
|
||||
it('automatically has Authorize Header add', async () => {
|
||||
const bundle = {
|
||||
authData: {
|
||||
username: 'user',
|
||||
password: 'secret',
|
||||
},
|
||||
};
|
||||
|
||||
const response = await appTester(App.authentication.test, bundle);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.request.headers.Authorization).toBe(
|
||||
'Basic dXNlcjpzZWNyZXQ='
|
||||
);
|
||||
});
|
||||
|
||||
it('fails on bad auth', async () => {
|
||||
const bundle = {
|
||||
authData: {
|
||||
username: 'user',
|
||||
password: 'badpwd',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await appTester(App.authentication.test, bundle);
|
||||
} catch (err) {
|
||||
expect(err.message).toContain(
|
||||
'The username and/or password you supplied is incorrect'
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw new Error('appTester should have thrown');
|
||||
});
|
||||
});
|
||||
42
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/basic.test.ts
vendored
Normal file
42
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/basic.test.ts
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import zapier from 'zapier-platform-core';
|
||||
|
||||
import App from '../index.js';
|
||||
const appTester = zapier.createAppTester(App);
|
||||
|
||||
describe('basic auth', () => {
|
||||
it('automatically has Authorize Header add', async () => {
|
||||
const bundle = {
|
||||
authData: {
|
||||
username: 'user',
|
||||
password: 'secret',
|
||||
},
|
||||
};
|
||||
|
||||
const response = await appTester(App.authentication.test, bundle);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.request.headers.Authorization).toBe(
|
||||
'Basic dXNlcjpzZWNyZXQ='
|
||||
);
|
||||
});
|
||||
|
||||
it('fails on bad auth', async () => {
|
||||
const bundle = {
|
||||
authData: {
|
||||
username: 'user',
|
||||
password: 'badpwd',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await appTester(App.authentication.test, bundle);
|
||||
} catch (err) {
|
||||
expect(err.message).toContain(
|
||||
'The username and/or password you supplied is incorrect'
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw new Error('appTester should have thrown');
|
||||
});
|
||||
});
|
||||
35
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/custom.test.js
vendored
Normal file
35
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/custom.test.js
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/* globals describe, it, expect */
|
||||
|
||||
const zapier = require('zapier-platform-core');
|
||||
|
||||
const App = require('../index');
|
||||
const appTester = zapier.createAppTester(App);
|
||||
|
||||
describe('custom auth', () => {
|
||||
it('passes authentication and returns json', async () => {
|
||||
const bundle = {
|
||||
authData: {
|
||||
apiKey: 'secret',
|
||||
},
|
||||
};
|
||||
|
||||
const response = await appTester(App.authentication.test, bundle);
|
||||
expect(response.data).toHaveProperty('username');
|
||||
});
|
||||
|
||||
it('fails on bad auth', async () => {
|
||||
const bundle = {
|
||||
authData: {
|
||||
apiKey: 'bad',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await appTester(App.authentication.test, bundle);
|
||||
} catch (error) {
|
||||
expect(error.message).toContain('The API Key you supplied is incorrect');
|
||||
return;
|
||||
}
|
||||
throw new Error('appTester should have thrown');
|
||||
});
|
||||
});
|
||||
34
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/custom.test.ts
vendored
Normal file
34
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/custom.test.ts
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import zapier from 'zapier-platform-core';
|
||||
|
||||
import App from '../index.js';
|
||||
const appTester = zapier.createAppTester(App);
|
||||
|
||||
describe('custom auth', () => {
|
||||
it('passes authentication and returns json', async () => {
|
||||
const bundle = {
|
||||
authData: {
|
||||
apiKey: 'secret',
|
||||
},
|
||||
};
|
||||
|
||||
const response = await appTester(App.authentication.test, bundle);
|
||||
expect(response.data).toHaveProperty('username');
|
||||
});
|
||||
|
||||
it('fails on bad auth', async () => {
|
||||
const bundle = {
|
||||
authData: {
|
||||
apiKey: 'bad',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await appTester(App.authentication.test, bundle);
|
||||
} catch (error) {
|
||||
expect(error.message).toContain('The API Key you supplied is incorrect');
|
||||
return;
|
||||
}
|
||||
throw new Error('appTester should have thrown');
|
||||
});
|
||||
});
|
||||
44
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/digest.test.js
vendored
Normal file
44
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/digest.test.js
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/* globals describe, it, expect */
|
||||
|
||||
const zapier = require('zapier-platform-core');
|
||||
|
||||
const App = require('../index');
|
||||
const appTester = zapier.createAppTester(App);
|
||||
|
||||
describe('digest auth', () => {
|
||||
it('correctly authenticates', async () => {
|
||||
// Try changing the values of username or password to see how the test method behaves
|
||||
const bundle = {
|
||||
authData: {
|
||||
username: 'myuser',
|
||||
password: 'mypass',
|
||||
},
|
||||
};
|
||||
|
||||
const response = await appTester(App.authentication.test, bundle);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.data.authorized).toBe(true);
|
||||
expect(response.data.user).toBe('myuser');
|
||||
});
|
||||
|
||||
it('fails on bad auth', async () => {
|
||||
// Try changing the values of username or password to see how the test method behaves
|
||||
const bundle = {
|
||||
authData: {
|
||||
username: 'user',
|
||||
password: 'badpwd',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await appTester(App.authentication.test, bundle);
|
||||
} catch (err) {
|
||||
expect(err.message).toContain(
|
||||
'The username and/or password you supplied is incorrect'
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw new Error('appTester should have thrown');
|
||||
});
|
||||
});
|
||||
43
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/digest.test.ts
vendored
Normal file
43
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/digest.test.ts
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import zapier from 'zapier-platform-core';
|
||||
|
||||
import App from '../index.js';
|
||||
const appTester = zapier.createAppTester(App);
|
||||
|
||||
describe('digest auth', () => {
|
||||
it('correctly authenticates', async () => {
|
||||
// Try changing the values of username or password to see how the test method behaves
|
||||
const bundle = {
|
||||
authData: {
|
||||
username: 'myuser',
|
||||
password: 'mypass',
|
||||
},
|
||||
};
|
||||
|
||||
const response = await appTester(App.authentication.test, bundle);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.data.authorized).toBe(true);
|
||||
expect(response.data.user).toBe('myuser');
|
||||
});
|
||||
|
||||
it('fails on bad auth', async () => {
|
||||
// Try changing the values of username or password to see how the test method behaves
|
||||
const bundle = {
|
||||
authData: {
|
||||
username: 'user',
|
||||
password: 'badpwd',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await appTester(App.authentication.test, bundle);
|
||||
} catch (err) {
|
||||
expect(err.message).toContain(
|
||||
'The username and/or password you supplied is incorrect'
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw new Error('appTester should have thrown');
|
||||
});
|
||||
});
|
||||
7
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/generic.test.js
vendored
Normal file
7
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/generic.test.js
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/* globals describe, it, expect */
|
||||
|
||||
describe('addition ', () => {
|
||||
it('should work', () => {
|
||||
expect(1 + 1).toEqual(2);
|
||||
});
|
||||
});
|
||||
65
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/oauth1.test.js
vendored
Normal file
65
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/oauth1.test.js
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/* globals describe, it, expect, beforeAll */
|
||||
|
||||
const zapier = require('zapier-platform-core');
|
||||
|
||||
zapier.tools.env.inject(); // read from the .env file
|
||||
|
||||
const App = require('../index');
|
||||
const appTester = zapier.createAppTester(App);
|
||||
|
||||
// You should create a `.env` file and populate it with your actual Trello
|
||||
// client ID and secret (called "API Key" and "API Secret" by Trello), which you
|
||||
// can find on https://trello.com/app-key.
|
||||
// The `.env` file should look like
|
||||
/*
|
||||
CLIENT_ID=<trello_api_key>
|
||||
CLIENT_SECRET=<trello_api_secret>
|
||||
*/
|
||||
// then you can delete the following 2 lines
|
||||
process.env.CLIENT_ID = process.env.CLIENT_ID || '<trello_api_key>';
|
||||
process.env.CLIENT_SECRET = process.env.CLIENT_SECRET || '<trello_api_secret>';
|
||||
|
||||
describe('oauth1 app', () => {
|
||||
beforeAll(() => {
|
||||
// It's a good idea to store your Client ID and Secret in the environment rather than in code.
|
||||
if (!(process.env.CLIENT_ID && process.env.CLIENT_SECRET)) {
|
||||
throw new Error(
|
||||
`Before running the tests, make sure CLIENT_ID and CLIENT_SECRET are available in the environment.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('fetch a request token', async () => {
|
||||
const bundle = {
|
||||
inputData: {
|
||||
// You should add your redirect URI to https://trello.com/app-key
|
||||
redirect_uri: 'https://zapier.com',
|
||||
},
|
||||
};
|
||||
const tokens = await appTester(
|
||||
App.authentication.oauth1Config.getRequestToken,
|
||||
bundle
|
||||
);
|
||||
expect(tokens).toHaveProperty('oauth_token');
|
||||
expect(tokens).toHaveProperty('oauth_token_secret');
|
||||
});
|
||||
|
||||
it('generates an authorize URL', async () => {
|
||||
const bundle = {
|
||||
// In production, these will be generated by Zapier and set automatically
|
||||
inputData: {
|
||||
oauth_token: '4444',
|
||||
redirect_uri: 'https://zapier.com/',
|
||||
},
|
||||
};
|
||||
|
||||
const authorizeUrl = await appTester(
|
||||
App.authentication.oauth1Config.authorizeUrl,
|
||||
bundle
|
||||
);
|
||||
|
||||
expect(authorizeUrl).toBe(
|
||||
'https://trello.com/1/OAuthAuthorizeToken?oauth_token=4444&name=Zapier%2FTrello%20OAuth1%20Test'
|
||||
);
|
||||
});
|
||||
});
|
||||
63
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/oauth1.test.ts
vendored
Normal file
63
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/oauth1.test.ts
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { describe, expect, it, beforeAll } from 'vitest';
|
||||
import zapier from 'zapier-platform-core';
|
||||
|
||||
import App from '../index.js';
|
||||
const appTester = zapier.createAppTester(App);
|
||||
|
||||
// Only defining the env vars here so the tests out of the box.
|
||||
// You should create a `.env` file and populate it with your actual Trello
|
||||
// client ID and secret (called "API Key" and "API Secret" by Trello), which you
|
||||
// can find on https://trello.com/app-key.
|
||||
// The `.env` file should look like
|
||||
/*
|
||||
CLIENT_ID=<trello_api_key>
|
||||
CLIENT_SECRET=<trello_api_secret>
|
||||
*/
|
||||
// then you can delete the following 2 lines
|
||||
process.env.CLIENT_ID = process.env.CLIENT_ID || '<trello_api_key>';
|
||||
process.env.CLIENT_SECRET = process.env.CLIENT_SECRET || '<trello_api_secret>';
|
||||
|
||||
describe('oauth1 app', () => {
|
||||
beforeAll(() => {
|
||||
// It's a good idea to store your Client ID and Secret in the environment rather than in code.
|
||||
if (!(process.env.CLIENT_ID && process.env.CLIENT_SECRET)) {
|
||||
throw new Error(
|
||||
`Before running the tests, make sure CLIENT_ID and CLIENT_SECRET are available in the environment.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('fetch a request token', async () => {
|
||||
const bundle = {
|
||||
inputData: {
|
||||
// You should add your redirect URI to https://trello.com/app-key
|
||||
redirect_uri: 'https://zapier.com',
|
||||
},
|
||||
};
|
||||
const tokens = await appTester(
|
||||
App.authentication.oauth1Config.getRequestToken,
|
||||
bundle
|
||||
);
|
||||
expect(tokens).toHaveProperty('oauth_token');
|
||||
expect(tokens).toHaveProperty('oauth_token_secret');
|
||||
});
|
||||
|
||||
it('generates an authorize URL', async () => {
|
||||
const bundle = {
|
||||
// In production, these will be generated by Zapier and set automatically
|
||||
inputData: {
|
||||
oauth_token: '4444',
|
||||
redirect_uri: 'https://zapier.com/',
|
||||
},
|
||||
};
|
||||
|
||||
const authorizeUrl = await appTester(
|
||||
App.authentication.oauth1Config.authorizeUrl,
|
||||
bundle
|
||||
);
|
||||
|
||||
expect(authorizeUrl).toBe(
|
||||
'https://trello.com/1/OAuthAuthorizeToken?oauth_token=4444&name=Zapier%2FTrello%20OAuth1%20Test'
|
||||
);
|
||||
});
|
||||
});
|
||||
118
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/oauth2.test.js
vendored
Normal file
118
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/oauth2.test.js
vendored
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
/* globals describe, it, expect, beforeAll */
|
||||
|
||||
const zapier = require('zapier-platform-core');
|
||||
|
||||
zapier.tools.env.inject(); // read from the .env file
|
||||
|
||||
const App = require('../index');
|
||||
const appTester = zapier.createAppTester(App);
|
||||
|
||||
// Only here so the tests out of the box.
|
||||
// You should create a `.env` file and populate it with the necessarily configuration
|
||||
// it should look like:
|
||||
/*
|
||||
CLIENT_ID=1234
|
||||
CLIENT_SECRET=asdf
|
||||
*/
|
||||
// then you can delete the following 2 lines
|
||||
process.env.CLIENT_ID = process.env.CLIENT_ID || '1234';
|
||||
process.env.CLIENT_SECRET = process.env.CLIENT_SECRET || 'asdf';
|
||||
|
||||
describe('oauth2 app', () => {
|
||||
beforeAll(() => {
|
||||
// It's a good idea to store your Client ID and Secret in the environment rather than in code.
|
||||
if (!(process.env.CLIENT_ID && process.env.CLIENT_SECRET)) {
|
||||
throw new Error(
|
||||
`Before running the tests, make sure CLIENT_ID and CLIENT_SECRET are available in the environment.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('generates an authorize URL', async () => {
|
||||
const bundle = {
|
||||
// In production, these will be generated by Zapier and set automatically
|
||||
inputData: {
|
||||
state: '4444',
|
||||
redirect_uri: 'https://zapier.com/',
|
||||
},
|
||||
environment: {
|
||||
CLIENT_ID: process.env.CLIENT_ID,
|
||||
CLIENT_SECRET: process.env.CLIENT_SECRET,
|
||||
},
|
||||
};
|
||||
|
||||
const authorizeUrl = await appTester(
|
||||
App.authentication.oauth2Config.authorizeUrl,
|
||||
bundle
|
||||
);
|
||||
|
||||
expect(authorizeUrl).toBe(
|
||||
'https://auth-json-server.zapier-staging.com/oauth/authorize?client_id=1234&state=4444&redirect_uri=https%3A%2F%2Fzapier.com%2F&response_type=code'
|
||||
);
|
||||
});
|
||||
|
||||
it('can fetch an access token', async () => {
|
||||
const bundle = {
|
||||
inputData: {
|
||||
// In production, Zapier passes along whatever code your API set in the query params when it redirects
|
||||
// the user's browser to the `redirect_uri`
|
||||
code: 'one_time_code',
|
||||
},
|
||||
environment: {
|
||||
CLIENT_ID: process.env.CLIENT_ID,
|
||||
CLIENT_SECRET: process.env.CLIENT_SECRET,
|
||||
},
|
||||
cleanedRequest: {
|
||||
querystring: {
|
||||
accountDomain: 'test-account',
|
||||
code: 'one_time_code',
|
||||
},
|
||||
},
|
||||
rawRequest: {
|
||||
querystring: '?accountDomain=test-account&code=one_time_code',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await appTester(
|
||||
App.authentication.oauth2Config.getAccessToken,
|
||||
bundle
|
||||
);
|
||||
|
||||
expect(result.access_token).toBe('a_token');
|
||||
expect(result.refresh_token).toBe('a_refresh_token');
|
||||
});
|
||||
|
||||
it('can refresh the access token', async () => {
|
||||
const bundle = {
|
||||
// In production, Zapier provides these. For testing, we have hard-coded them.
|
||||
// When writing tests for your own app, you should consider exporting them and doing process.env.MY_ACCESS_TOKEN
|
||||
authData: {
|
||||
access_token: 'a_token',
|
||||
refresh_token: 'a_refresh_token',
|
||||
},
|
||||
environment: {
|
||||
CLIENT_ID: process.env.CLIENT_ID,
|
||||
CLIENT_SECRET: process.env.CLIENT_SECRET,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await appTester(
|
||||
App.authentication.oauth2Config.refreshAccessToken,
|
||||
bundle
|
||||
);
|
||||
expect(result.access_token).toBe('a_token');
|
||||
});
|
||||
|
||||
it('includes the access token in future requests', async () => {
|
||||
const bundle = {
|
||||
authData: {
|
||||
access_token: 'a_token',
|
||||
refresh_token: 'a_refresh_token',
|
||||
},
|
||||
};
|
||||
|
||||
const response = await appTester(App.authentication.test, bundle);
|
||||
expect(response.data).toHaveProperty('username');
|
||||
expect(response.data.username).toBe('Bret');
|
||||
});
|
||||
});
|
||||
115
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/oauth2.test.ts
vendored
Normal file
115
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/oauth2.test.ts
vendored
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import { describe, expect, it, beforeAll } from 'vitest';
|
||||
import zapier from 'zapier-platform-core';
|
||||
|
||||
import App from '../index.js';
|
||||
const appTester = zapier.createAppTester(App);
|
||||
|
||||
// Only defining the env vars here so the tests out of the box.
|
||||
// You should create a `.env` file and populate it with the necessarily configuration
|
||||
// it should look like:
|
||||
/*
|
||||
CLIENT_ID=1234
|
||||
CLIENT_SECRET=asdf
|
||||
*/
|
||||
// then you can delete the following 2 lines
|
||||
process.env.CLIENT_ID = process.env.CLIENT_ID || '1234';
|
||||
process.env.CLIENT_SECRET = process.env.CLIENT_SECRET || 'asdf';
|
||||
|
||||
describe('authentication', () => {
|
||||
beforeAll(() => {
|
||||
// It's a good idea to store your Client ID and Secret in the environment rather than in code.
|
||||
if (!(process.env.CLIENT_ID && process.env.CLIENT_SECRET)) {
|
||||
throw new Error(
|
||||
`Before running the tests, make sure CLIENT_ID and CLIENT_SECRET are available in the environment.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('generates an authorize URL', async () => {
|
||||
const bundle = {
|
||||
// In production, these will be generated by Zapier and set automatically
|
||||
inputData: {
|
||||
state: '4444',
|
||||
redirect_uri: 'https://zapier.com/',
|
||||
},
|
||||
environment: {
|
||||
CLIENT_ID: process.env.CLIENT_ID,
|
||||
CLIENT_SECRET: process.env.CLIENT_SECRET,
|
||||
},
|
||||
};
|
||||
|
||||
const authorizeUrl = await appTester(
|
||||
App.authentication.oauth2Config.authorizeUrl,
|
||||
bundle,
|
||||
);
|
||||
|
||||
expect(authorizeUrl).toBe(
|
||||
'https://auth-json-server.zapier-staging.com/oauth/authorize?client_id=1234&state=4444&redirect_uri=https%3A%2F%2Fzapier.com%2F&response_type=code',
|
||||
);
|
||||
});
|
||||
|
||||
it('can fetch an access token', async () => {
|
||||
const bundle = {
|
||||
inputData: {
|
||||
// In production, Zapier passes along whatever code your API set in the query params when it redirects
|
||||
// the user's browser to the `redirect_uri`
|
||||
code: 'one_time_code',
|
||||
},
|
||||
environment: {
|
||||
CLIENT_ID: process.env.CLIENT_ID,
|
||||
CLIENT_SECRET: process.env.CLIENT_SECRET,
|
||||
},
|
||||
cleanedRequest: {
|
||||
querystring: {
|
||||
accountDomain: 'test-account',
|
||||
code: 'one_time_code',
|
||||
},
|
||||
},
|
||||
rawRequest: {
|
||||
querystring: '?accountDomain=test-account&code=one_time_code',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await appTester(
|
||||
App.authentication.oauth2Config.getAccessToken,
|
||||
bundle,
|
||||
);
|
||||
|
||||
expect(result.access_token).toBe('a_token');
|
||||
expect(result.refresh_token).toBe('a_refresh_token');
|
||||
});
|
||||
|
||||
it('can refresh the access token', async () => {
|
||||
const bundle = {
|
||||
// In production, Zapier provides these. For testing, we have hard-coded them.
|
||||
// When writing tests for your own app, you should consider exporting them and doing process.env.MY_ACCESS_TOKEN
|
||||
authData: {
|
||||
access_token: 'a_token',
|
||||
refresh_token: 'a_refresh_token',
|
||||
},
|
||||
environment: {
|
||||
CLIENT_ID: process.env.CLIENT_ID,
|
||||
CLIENT_SECRET: process.env.CLIENT_SECRET,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await appTester(
|
||||
App.authentication.oauth2Config.refreshAccessToken,
|
||||
bundle,
|
||||
);
|
||||
expect(result.access_token).toBe('a_token');
|
||||
});
|
||||
|
||||
it('includes the access token in future requests', async () => {
|
||||
const bundle = {
|
||||
authData: {
|
||||
access_token: 'a_token',
|
||||
refresh_token: 'a_refresh_token',
|
||||
},
|
||||
};
|
||||
|
||||
const response = await appTester(App.authentication.test, bundle);
|
||||
expect(response.data).toHaveProperty('username');
|
||||
expect(response.data.username).toBe('Bret');
|
||||
});
|
||||
});
|
||||
37
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/session.test.js
vendored
Normal file
37
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/session.test.js
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/* globals describe, it, expect */
|
||||
|
||||
const zapier = require('zapier-platform-core');
|
||||
|
||||
const App = require('../index');
|
||||
const appTester = zapier.createAppTester(App);
|
||||
|
||||
describe('session auth app', () => {
|
||||
it('has an exchange for username/password', async () => {
|
||||
const bundle = {
|
||||
authData: {
|
||||
username: 'bryan',
|
||||
password: 'hunter2',
|
||||
},
|
||||
};
|
||||
|
||||
const newAuthData = await appTester(
|
||||
App.authentication.sessionConfig.perform,
|
||||
bundle
|
||||
);
|
||||
|
||||
expect(newAuthData.sessionKey).toBe('secret');
|
||||
});
|
||||
|
||||
it('has auth details added to every request', async () => {
|
||||
const bundle = {
|
||||
authData: {
|
||||
sessionKey: 'secret',
|
||||
},
|
||||
};
|
||||
|
||||
const response = await appTester(App.authentication.test, bundle);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.request.headers['X-API-Key']).toBe('secret');
|
||||
});
|
||||
});
|
||||
36
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/session.test.ts
vendored
Normal file
36
vendor/zapier-platform/packages/cli/src/generators/templates/authTests/session.test.ts
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import zapier from 'zapier-platform-core';
|
||||
|
||||
import App from '../index.js';
|
||||
const appTester = zapier.createAppTester(App);
|
||||
|
||||
describe('session auth app', () => {
|
||||
it('has an exchange for username/password', async () => {
|
||||
const bundle = {
|
||||
authData: {
|
||||
username: 'bryan',
|
||||
password: 'hunter2',
|
||||
},
|
||||
};
|
||||
|
||||
const newAuthData = await appTester(
|
||||
App.authentication.sessionConfig.perform,
|
||||
bundle
|
||||
);
|
||||
|
||||
expect(newAuthData.sessionKey).toBe('secret');
|
||||
});
|
||||
|
||||
it('has auth details added to every request', async () => {
|
||||
const bundle = {
|
||||
authData: {
|
||||
sessionKey: 'secret',
|
||||
},
|
||||
};
|
||||
|
||||
const response = await appTester(App.authentication.test, bundle);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.request.headers['X-API-Key']).toBe('secret');
|
||||
});
|
||||
});
|
||||
5
vendor/zapier-platform/packages/cli/src/generators/templates/callback/README.md
vendored
Normal file
5
vendor/zapier-platform/packages/cli/src/generators/templates/callback/README.md
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# The "Callback" Template
|
||||
|
||||
This example has a create showcasing the `performResume` callback function.
|
||||
|
||||
Find out more in the docs: https://github.com/zapier/zapier-platform/blob/main/packages/cli#zgeneratecallbackurl.
|
||||
73
vendor/zapier-platform/packages/cli/src/generators/templates/callback/creates/prediction.js
vendored
Normal file
73
vendor/zapier-platform/packages/cli/src/generators/templates/callback/creates/prediction.js
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// We recommend writing your creates separate like this and rolling them
|
||||
// into the App definition at the end.
|
||||
module.exports = {
|
||||
key: 'prediction',
|
||||
|
||||
// You'll want to provide some helpful display labels and descriptions
|
||||
// for users. Zapier will put them into the UX.
|
||||
noun: 'Prediction',
|
||||
display: {
|
||||
label: 'Create Prediction',
|
||||
description: 'Creates a new prediction.',
|
||||
},
|
||||
|
||||
// `operation` is where the business logic goes.
|
||||
operation: {
|
||||
inputFields: [
|
||||
{
|
||||
key: 'question',
|
||||
required: true,
|
||||
type: 'string',
|
||||
helpText: 'Provide a "Yes" or "No" question to ask the Magic 8-Ball.',
|
||||
},
|
||||
],
|
||||
perform: (z, bundle) => {
|
||||
const promise = z.request({
|
||||
url: 'https://auth-json-server.zapier-staging.com/magic',
|
||||
method: 'POST',
|
||||
body: {
|
||||
callbackUrl: z.generateCallbackUrl(),
|
||||
},
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
|
||||
// This is NOT how you normally do authentication. This is just to demo how to write a create here.
|
||||
// Refer to this doc to set up authentication:
|
||||
// https://docs.zapier.com/platform/reference/cli-docs#authentication
|
||||
'X-API-Key': 'secret',
|
||||
},
|
||||
});
|
||||
|
||||
return promise.then((response) => ({ ...response.data, extra: 'data' }));
|
||||
},
|
||||
|
||||
performResume: (z, bundle) => {
|
||||
// The original output from perform is available in bundle.outputData.
|
||||
// The data POSTed to the callbackUrl is in bundle.cleanedRequest.
|
||||
// The full request object corresponding to bundle.cleanedRequest can be found in bundle.rawRequest.
|
||||
const { extra, ...originalOutput } = bundle.outputData;
|
||||
// The following line will return an object containing the contents of the original API response to the
|
||||
// request from the perform function merged with the contents of the new request from the API.
|
||||
return { ...originalOutput, ...bundle.cleanedRequest };
|
||||
},
|
||||
|
||||
// In cases where Zapier needs to show an example record to the user, but we are unable to get a live example
|
||||
// from the API, Zapier will fallback to this hard-coded sample. It should reflect the data structure of
|
||||
// returned records, and have obviously dummy values that we can show to any user.
|
||||
sample: {
|
||||
callbackUrl: 'http://zapier.com/hooks/catch/-1234/abcdef/',
|
||||
status: 'success',
|
||||
result: 'Ask again later.',
|
||||
},
|
||||
|
||||
// If the resource can have fields that are custom on a per-user basis, define a function to fetch the custom
|
||||
// field definitions. The result will be used to augment the sample.
|
||||
// outputFields: () => { return []; }
|
||||
// Alternatively, a static field definition should be provided, to specify labels for the fields
|
||||
outputFields: [
|
||||
{ key: 'callbackUrl', label: 'Callback URL' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'result', label: 'Predicted Result' },
|
||||
],
|
||||
},
|
||||
};
|
||||
17
vendor/zapier-platform/packages/cli/src/generators/templates/callback/index.js
vendored
Normal file
17
vendor/zapier-platform/packages/cli/src/generators/templates/callback/index.js
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
const prediction = require('./creates/prediction');
|
||||
|
||||
// Now we can roll up all our behaviors in an App.
|
||||
const App = {
|
||||
// This is just shorthand to reference the installed dependencies you have. Zapier will
|
||||
// need to know these before we can upload
|
||||
version: require('./package.json').version,
|
||||
platformVersion: require('zapier-platform-core').version,
|
||||
|
||||
// If you want your creates to show up, you better include it here!
|
||||
creates: {
|
||||
[prediction.key]: prediction,
|
||||
},
|
||||
};
|
||||
|
||||
// Finally, export the app.
|
||||
module.exports = App;
|
||||
40
vendor/zapier-platform/packages/cli/src/generators/templates/callback/test/creates.test.js
vendored
Normal file
40
vendor/zapier-platform/packages/cli/src/generators/templates/callback/test/creates.test.js
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/* globals describe, expect, test */
|
||||
|
||||
const zapier = require('zapier-platform-core');
|
||||
|
||||
const App = require('../index');
|
||||
const appTester = zapier.createAppTester(App);
|
||||
zapier.tools.env.inject();
|
||||
|
||||
describe('creates', () => {
|
||||
test('perform function returns intermediate data', async () => {
|
||||
const bundle = { inputData: { question: 'Will this work?' } };
|
||||
const result = await appTester(App.creates.prediction.operation.perform, bundle);
|
||||
expect(result).toMatchObject({
|
||||
status: "...thinking...",
|
||||
callbackUrl: "https://auth-json-server.zapier-staging.com/echo",
|
||||
extra: "data",
|
||||
});
|
||||
});
|
||||
|
||||
test('performResume function returns "final" data', async () => {
|
||||
const bundle = {
|
||||
outputData: {
|
||||
callbackUrl: 'https://auth-json-server.zapier-staging.com/echo',
|
||||
status: '...thinking...',
|
||||
extra: 'data',
|
||||
},
|
||||
cleanedRequest: {
|
||||
status: 'success',
|
||||
result: 'Ask again later.',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await appTester(App.creates.prediction.operation.performResume, bundle);
|
||||
expect(result).toMatchObject({
|
||||
status: 'success',
|
||||
result: 'Ask again later.',
|
||||
callbackUrl: 'https://auth-json-server.zapier-staging.com/echo'
|
||||
});
|
||||
});
|
||||
});
|
||||
107
vendor/zapier-platform/packages/cli/src/generators/templates/dynamic-dropdown/README.md
vendored
Normal file
107
vendor/zapier-platform/packages/cli/src/generators/templates/dynamic-dropdown/README.md
vendored
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# dynamic-dropdown
|
||||
|
||||
This example integration demonstrates how to create **dynamic dropdowns** (also known as dynamic choices) in Zapier integrations.
|
||||
|
||||
## Dynamic Dropdown Patterns
|
||||
|
||||
There are two ways to implement dynamic dropdowns:
|
||||
|
||||
### 1. Trigger-based (Legacy Pattern)
|
||||
|
||||
Uses a separate trigger to fetch choices. Reference it with the `dynamic` property:
|
||||
|
||||
```javascript
|
||||
{
|
||||
key: 'species_id',
|
||||
type: 'integer',
|
||||
label: 'Species',
|
||||
dynamic: 'species.id.name', // Format: "triggerKey.idField.labelField"
|
||||
}
|
||||
```
|
||||
|
||||
The trigger (`species`) fetches data, and Zapier uses `id` for the value and `name` for the display label.
|
||||
|
||||
### 2. Perform-based (New Pattern)
|
||||
|
||||
Uses a function to fetch choices directly. Define it with `choices.perform`:
|
||||
|
||||
```javascript
|
||||
{
|
||||
key: 'planet_id',
|
||||
type: 'integer',
|
||||
label: 'Home Planet',
|
||||
resource: 'planet', // Explicit resource linking (see below)
|
||||
choices: {
|
||||
perform: getPlanetChoices,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### Resource Linking
|
||||
|
||||
The `resource` property explicitly links an input field to a resource. This is particularly important for perform-based dropdowns since they don't have a `dynamic` property to derive the resource from.
|
||||
|
||||
```javascript
|
||||
{
|
||||
key: 'spreadsheet_id',
|
||||
resource: 'spreadsheet',
|
||||
choices: { perform: getSpreadsheets },
|
||||
}
|
||||
```
|
||||
|
||||
The perform function must return:
|
||||
|
||||
```javascript
|
||||
{
|
||||
results: [
|
||||
{ id: '1', label: 'Tatooine' },
|
||||
{ id: '2', label: 'Alderaan' },
|
||||
],
|
||||
paging_token: 'https://api.example.com/planets?page=2', // or null if no more pages
|
||||
}
|
||||
```
|
||||
|
||||
#### Pagination Support
|
||||
|
||||
The perform function receives `bundle.meta.paging_token` for subsequent page requests:
|
||||
|
||||
```javascript
|
||||
const getPlanetChoices = async (z, bundle) => {
|
||||
// First request: paging_token is undefined
|
||||
// Subsequent requests: paging_token is the value you returned previously
|
||||
const url = bundle.meta.paging_token || 'https://api.example.com/planets';
|
||||
|
||||
const response = await z.request({ url });
|
||||
|
||||
return {
|
||||
results: response.data.results.map((item) => ({
|
||||
id: item.id,
|
||||
label: item.name,
|
||||
})),
|
||||
// Return null when there are no more pages
|
||||
paging_token: response.data.next,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## This Example
|
||||
|
||||
This integration uses the [Star Wars API](https://swapi.dev/) to demonstrate:
|
||||
|
||||
- **Species dropdown** - Trigger-based pattern using the `species` trigger
|
||||
- **Planet dropdown** - Perform-based pattern with pagination and explicit `resource` linking
|
||||
|
||||
## Getting Started
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run tests
|
||||
zapier-platform test
|
||||
|
||||
# Push to Zapier
|
||||
zapier-platform push
|
||||
```
|
||||
|
||||
Find out more on the latest docs: https://docs.zapier.com/platform
|
||||
12
vendor/zapier-platform/packages/cli/src/generators/templates/dynamic-dropdown/index.js
vendored
Normal file
12
vendor/zapier-platform/packages/cli/src/generators/templates/dynamic-dropdown/index.js
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
const people = require('./triggers/people');
|
||||
const species = require('./triggers/species');
|
||||
|
||||
module.exports = {
|
||||
version: require('./package.json').version,
|
||||
platformVersion: require('zapier-platform-core').version,
|
||||
|
||||
triggers: {
|
||||
[people.key]: people,
|
||||
[species.key]: species,
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/* globals describe, expect, test */
|
||||
|
||||
const zapier = require('zapier-platform-core');
|
||||
|
||||
const App = require('../index');
|
||||
const appTester = zapier.createAppTester(App);
|
||||
zapier.tools.env.inject();
|
||||
|
||||
describe('triggers', () => {
|
||||
test('species', async () => {
|
||||
const bundle = {
|
||||
inputData: {},
|
||||
meta: {},
|
||||
};
|
||||
|
||||
const results = await appTester(
|
||||
App.triggers.species.operation.perform,
|
||||
bundle
|
||||
);
|
||||
expect(results.length).toBeGreaterThan(1);
|
||||
|
||||
const firstSpecies = results[0];
|
||||
expect(firstSpecies.id).toBe(1);
|
||||
expect(firstSpecies.name).toBe('Human');
|
||||
});
|
||||
|
||||
test('people', async () => {
|
||||
const bundle = {
|
||||
inputData: {
|
||||
species: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const results = await appTester(
|
||||
App.triggers.people.operation.perform,
|
||||
bundle
|
||||
);
|
||||
expect(results.length).toBeGreaterThan(1);
|
||||
|
||||
const firstPerson = results[0];
|
||||
expect(firstPerson.id).toBe(1);
|
||||
expect(firstPerson.name).toBe('Luke Skywalker');
|
||||
});
|
||||
});
|
||||
120
vendor/zapier-platform/packages/cli/src/generators/templates/dynamic-dropdown/triggers/people.js
vendored
Normal file
120
vendor/zapier-platform/packages/cli/src/generators/templates/dynamic-dropdown/triggers/people.js
vendored
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
const { extractID } = require('../utils');
|
||||
|
||||
/**
|
||||
* PERFORM-BASED choices WITH PAGINATION (NEW pattern)
|
||||
* Fetches planets from the Star Wars API with pagination support.
|
||||
*
|
||||
* - bundle.meta.paging_token is a full URL from the previous response
|
||||
* - Return paging_token as the API's next page URL (or null if no more pages)
|
||||
*
|
||||
* MUST return: { results: [...], paging_token: string|null }
|
||||
*/
|
||||
const getPlanetChoices = async (z, bundle) => {
|
||||
// paging_token is a full URL to the next page (from SWAPI's "next" field)
|
||||
// First page: paging_token is undefined/null, use default URL
|
||||
const url = bundle.meta.paging_token || 'https://swapi.dev/api/planets/';
|
||||
|
||||
const response = await z.request({ url });
|
||||
const data = response.data;
|
||||
|
||||
// SWAPI returns: { results: [...], next: "url" or null }
|
||||
return {
|
||||
results: data.results.map((planet) => ({
|
||||
id: extractID(planet.url),
|
||||
label: planet.name,
|
||||
})),
|
||||
// Return SWAPI's next URL as our paging_token
|
||||
paging_token: data.next,
|
||||
};
|
||||
};
|
||||
|
||||
// Fetches a list of records from the endpoint
|
||||
const perform = async (z, bundle) => {
|
||||
// Ideally, we should poll through all the pages of results, but in this
|
||||
// example we're going to omit that part. Thus, this trigger only "see" the
|
||||
// people in their first page of results.
|
||||
const response = await z.request({ url: 'https://swapi.info/api/people/' });
|
||||
let peopleArray = response.data;
|
||||
|
||||
if (bundle.inputData.species_id) {
|
||||
// The Zap's setup has requested a specific species of person. Since the
|
||||
// API/endpoint can't perform the filtering, we'll perform it here, within
|
||||
// the integration, and return the matching objects/records back to Zapier.
|
||||
peopleArray = peopleArray.filter((person) => {
|
||||
let speciesID;
|
||||
if (!person.species || !person.species.length) {
|
||||
speciesID = 1; // Assume human if species is not provided
|
||||
} else {
|
||||
speciesID = extractID(person.species[0]);
|
||||
}
|
||||
return speciesID === bundle.inputData.species_id;
|
||||
});
|
||||
}
|
||||
|
||||
if (bundle.inputData.planet_id) {
|
||||
// The Zap's setup has requested a specific home planet. Filter people by
|
||||
// homeworld (SWAPI people have a homeworld URL).
|
||||
peopleArray = peopleArray.filter((person) => {
|
||||
if (!person.homeworld) return false;
|
||||
const homeworldID = extractID(person.homeworld);
|
||||
return homeworldID === bundle.inputData.planet_id;
|
||||
});
|
||||
}
|
||||
|
||||
return peopleArray.map((person) => {
|
||||
person.id = extractID(person.url);
|
||||
return person;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
key: 'people',
|
||||
noun: 'person',
|
||||
display: {
|
||||
label: 'New Person',
|
||||
description: 'Triggers when a new person is added.',
|
||||
},
|
||||
|
||||
operation: {
|
||||
inputFields: [
|
||||
// TRIGGER-BASED dynamic dropdown (legacy pattern)
|
||||
// Uses a separate trigger to fetch choices
|
||||
{
|
||||
key: 'species_id',
|
||||
type: 'integer',
|
||||
label: 'Species (trigger-based)',
|
||||
helpText:
|
||||
'Filter by species. Uses trigger-based dynamic dropdown (dynamic: "species.id.name").',
|
||||
dynamic: 'species.id.name',
|
||||
altersDynamicFields: true,
|
||||
},
|
||||
// PERFORM-BASED dynamic dropdown WITH PAGINATION (new pattern)
|
||||
// Uses a function to fetch choices directly
|
||||
{
|
||||
key: 'planet_id',
|
||||
type: 'integer',
|
||||
label: 'Home Planet (perform-based)',
|
||||
helpText:
|
||||
'Filter by home planet. Uses perform-based dynamic dropdown with pagination support.',
|
||||
resource: 'planet', // Explicit resource linking for perform-based dropdowns
|
||||
choices: {
|
||||
perform: getPlanetChoices,
|
||||
},
|
||||
},
|
||||
],
|
||||
perform,
|
||||
sample: {
|
||||
id: '1',
|
||||
name: 'Luke Skywalker',
|
||||
birth_year: '19 BBY',
|
||||
eye_color: 'Blue',
|
||||
gender: 'Male',
|
||||
hair_color: 'Blond',
|
||||
height: '172',
|
||||
mass: '77',
|
||||
skin_color: 'Fair',
|
||||
created: '2014-12-09T13:50:51.644000Z',
|
||||
edited: '2014-12-10T13:52:43.172000Z',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
const { extractID } = require('../utils');
|
||||
|
||||
// Fetches a list of records from the endpoint
|
||||
const perform = async (z, bundle) => {
|
||||
const request = {
|
||||
url: 'https://swapi.info/api/species/',
|
||||
params: {},
|
||||
};
|
||||
|
||||
// This API returns things in "pages" of results
|
||||
if (bundle.meta.page) {
|
||||
request.params.page = 1 + bundle.meta.page;
|
||||
}
|
||||
|
||||
const response = await z.request(request);
|
||||
const speciesArray = response.data;
|
||||
return speciesArray.map((species) => {
|
||||
species.id = extractID(species.url);
|
||||
return species;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
key: 'species',
|
||||
noun: 'Species',
|
||||
display: {
|
||||
label: 'List of Species',
|
||||
description:
|
||||
'This is a hidden trigger, and is used in a Dynamic Dropdown of another trigger.',
|
||||
hidden: true,
|
||||
},
|
||||
|
||||
operation: {
|
||||
// Since this is a "hidden" trigger, there aren't any inputFields needed
|
||||
perform,
|
||||
// The folowing is a "hint" to the Zap Editor that this trigger returns data
|
||||
// "in pages", and that the UI should display an option to "load more" to
|
||||
// the human.
|
||||
canPaginate: true,
|
||||
},
|
||||
};
|
||||
12
vendor/zapier-platform/packages/cli/src/generators/templates/dynamic-dropdown/utils.js
vendored
Normal file
12
vendor/zapier-platform/packages/cli/src/generators/templates/dynamic-dropdown/utils.js
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Some handy stuff that's used in various places
|
||||
|
||||
// Extract the numeric ID from a URL like 'https://swapi.dev/api/people/1/'
|
||||
const extractID = (urlString) => {
|
||||
const match = urlString.match(/\/(\d+)\/?$/);
|
||||
if (match) {
|
||||
return parseInt(match[1]);
|
||||
}
|
||||
throw new Error(`ID not found in URL: ${urlString}`);
|
||||
};
|
||||
|
||||
module.exports = { extractID };
|
||||
5
vendor/zapier-platform/packages/cli/src/generators/templates/files/README.md
vendored
Normal file
5
vendor/zapier-platform/packages/cli/src/generators/templates/files/README.md
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# The "files" Template
|
||||
|
||||
This example has a trigger and a create showcasing file handling.
|
||||
|
||||
Find out more in the docs: https://github.com/zapier/zapier-platform/blob/main/packages/cli/README.md#stashing-files.
|
||||
61
vendor/zapier-platform/packages/cli/src/generators/templates/files/creates/uploadFile_v10.js
vendored
Normal file
61
vendor/zapier-platform/packages/cli/src/generators/templates/files/creates/uploadFile_v10.js
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
const http = require('https'); // require('http') if your URL is not https
|
||||
|
||||
const FormData = require('form-data');
|
||||
|
||||
// Getting a stream directly from http. This only works on core 10+. For core
|
||||
// 9.x compatible code, see uploadFile_v9.js.
|
||||
const makeDownloadStream = (url) =>
|
||||
new Promise((resolve, reject) => {
|
||||
http.request(url, (res) => {
|
||||
// We can risk missing the first n bytes if we don't pause!
|
||||
res.pause();
|
||||
resolve(res);
|
||||
}).on('error', reject).end();
|
||||
});
|
||||
|
||||
const perform = async (z, bundle) => {
|
||||
// bundle.inputData.file will in fact be an URL where the file data can be
|
||||
// downloaded from which we do via a stream
|
||||
const stream = await makeDownloadStream(bundle.inputData.file, z);
|
||||
|
||||
const form = new FormData();
|
||||
form.append('filename', bundle.inputData.filename);
|
||||
form.append('file', stream);
|
||||
|
||||
// All set! Resume the stream
|
||||
stream.resume();
|
||||
|
||||
const response = await z.request({
|
||||
url: 'https://auth-json-server.zapier-staging.com/upload',
|
||||
method: 'POST',
|
||||
body: form,
|
||||
headers: {
|
||||
// DO NOT do auth like this! We do this here because this is a file
|
||||
// uploading example so the auth is not the point.
|
||||
'x-api-key': 'secret',
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
key: 'uploadFile_v10',
|
||||
noun: 'File',
|
||||
display: {
|
||||
label: 'Upload File v10',
|
||||
description: 'Uploads a file. Only works on zapier-platform-core v10+.',
|
||||
},
|
||||
operation: {
|
||||
inputFields: [
|
||||
{ key: 'filename', required: true, type: 'string', label: 'Filename' },
|
||||
{ key: 'file', required: true, type: 'file', label: 'File' },
|
||||
],
|
||||
perform,
|
||||
sample: {
|
||||
id: 1,
|
||||
filename: 'example.pdf',
|
||||
file: 'SAMPLE FILE',
|
||||
},
|
||||
},
|
||||
};
|
||||
80
vendor/zapier-platform/packages/cli/src/generators/templates/files/creates/uploadFile_v9.js
vendored
Normal file
80
vendor/zapier-platform/packages/cli/src/generators/templates/files/creates/uploadFile_v9.js
vendored
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
const { randomBytes } = require('crypto');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const fetch = require('node-fetch');
|
||||
const FormData = require('form-data');
|
||||
|
||||
// Download the HTTP URL to a local temporary file, and make a readable stream
|
||||
// from it. This should work compatibly for all core versions. But if you're
|
||||
// using core v10+, we recommend to use the implementation of uploadFile_v10.js.
|
||||
const makeDownloadStream = async (url) => {
|
||||
// Create a temp file to store the downloaded file
|
||||
const filename = randomBytes(16).toString('hex');
|
||||
const tmpFilePath = path.join(os.tmpdir(), filename);
|
||||
const dest = fs.createWriteStream(tmpFilePath);
|
||||
|
||||
const response = await fetch(url);
|
||||
|
||||
// Download the file to the temp file. When finished, open a readable stream
|
||||
// from that temp file.
|
||||
return new Promise((resolve, reject) => {
|
||||
response.body
|
||||
.pipe(dest)
|
||||
.on('close', () => {
|
||||
const stream = fs.createReadStream(tmpFilePath).on('close', () => {
|
||||
// Delete the file once the stream is read
|
||||
fs.unlinkSync(tmpFilePath);
|
||||
});
|
||||
resolve(stream);
|
||||
})
|
||||
.on('error', reject);
|
||||
});
|
||||
};
|
||||
|
||||
const perform = async (z, bundle) => {
|
||||
const form = new FormData();
|
||||
|
||||
form.append('filename', bundle.inputData.filename);
|
||||
|
||||
// bundle.inputData.file will in fact be an URL where the file data can be
|
||||
// downloaded from which we do via a stream
|
||||
const stream = await makeDownloadStream(bundle.inputData.file, z);
|
||||
form.append('file', stream);
|
||||
|
||||
const response = await z.request({
|
||||
url: 'https://auth-json-server.zapier-staging.com/upload',
|
||||
method: 'POST',
|
||||
body: form,
|
||||
headers: {
|
||||
// DO NOT do auth like this! We do this here because this is a file
|
||||
// uploading example so the auth is not the point.
|
||||
'x-api-key': 'secret',
|
||||
},
|
||||
});
|
||||
|
||||
return response.json;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
key: 'uploadFile_v9',
|
||||
noun: 'File',
|
||||
display: {
|
||||
label: 'Upload File v9',
|
||||
description:
|
||||
'Uploads a file. Compatible with all versions of zapier-platform-core.',
|
||||
},
|
||||
operation: {
|
||||
inputFields: [
|
||||
{ key: 'filename', required: true, type: 'string', label: 'Filename' },
|
||||
{ key: 'file', required: true, type: 'file', label: 'File' },
|
||||
],
|
||||
perform,
|
||||
sample: {
|
||||
id: 1,
|
||||
filename: 'example.pdf',
|
||||
file: 'SAMPLE FILE',
|
||||
},
|
||||
},
|
||||
};
|
||||
16
vendor/zapier-platform/packages/cli/src/generators/templates/files/hydrators.js
vendored
Normal file
16
vendor/zapier-platform/packages/cli/src/generators/templates/files/hydrators.js
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
module.exports = {
|
||||
downloadFile: async (z, bundle) => {
|
||||
// Use standard auth to request the file
|
||||
const filePromise = z.request({
|
||||
url: bundle.inputData.url,
|
||||
raw: true,
|
||||
});
|
||||
|
||||
// When `raw` is true, the result of z.request() can be passed to
|
||||
// z.stashFile(). z.stashFile() will upload the file to a Zapier-owned S3
|
||||
// bucket and return a promise of an S3 URL that allows Zapier to get the
|
||||
// file without auth. If your file URL is permanently publicly available,
|
||||
// you may skip z.stashFile() and return that URL directly here.
|
||||
return z.stashFile(filePromise);
|
||||
},
|
||||
};
|
||||
25
vendor/zapier-platform/packages/cli/src/generators/templates/files/index.js
vendored
Normal file
25
vendor/zapier-platform/packages/cli/src/generators/templates/files/index.js
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
const hydrators = require('./hydrators');
|
||||
const newFile = require('./triggers/newFile');
|
||||
const uploadFileV10 = require('./creates/uploadFile_v10');
|
||||
const uploadFileV9 = require('./creates/uploadFile_v9');
|
||||
|
||||
module.exports = {
|
||||
// This is just shorthand to reference the installed dependencies you have.
|
||||
// Zapier will need to know these before we can upload.
|
||||
version: require('./package.json').version,
|
||||
platformVersion: require('zapier-platform-core').version,
|
||||
|
||||
// Any hydrators go here
|
||||
hydrators,
|
||||
|
||||
// If you want your triggers to show up, you better include it here!
|
||||
triggers: {
|
||||
[newFile.key]: newFile,
|
||||
},
|
||||
|
||||
// If you want your creates to show up, you better include it here!
|
||||
creates: {
|
||||
[uploadFileV10.key]: uploadFileV10,
|
||||
[uploadFileV9.key]: uploadFileV9,
|
||||
},
|
||||
};
|
||||
60
vendor/zapier-platform/packages/cli/src/generators/templates/files/test/creates.test.js
vendored
Normal file
60
vendor/zapier-platform/packages/cli/src/generators/templates/files/test/creates.test.js
vendored
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/* globals describe, expect, test */
|
||||
|
||||
const zapier = require('zapier-platform-core');
|
||||
|
||||
const App = require('../index');
|
||||
const appTester = zapier.createAppTester(App);
|
||||
zapier.tools.env.inject();
|
||||
|
||||
const CORE_VERSION = zapier.version.split('.').map((s) => parseInt(s));
|
||||
|
||||
const FILE_URL =
|
||||
'https://cdn.zapier.com/storage/files/f6679cf77afeaf6b8426de8d7b9642fc.pdf';
|
||||
|
||||
// This is what you get when doing `curl <FILE_URL> | sha1sum`
|
||||
const EXPECTED_SHA1 = '3cf58b42a0fb1b7cc58de8110096841ece967530';
|
||||
|
||||
describe('uploadFile', () => {
|
||||
test('upload file v10', async () => {
|
||||
if (CORE_VERSION[0] < 10) {
|
||||
console.warn(
|
||||
`skipped because this only works on core v10+ and you're on ${zapier.version}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const bundle = {
|
||||
inputData: {
|
||||
filename: 'sample.pdf',
|
||||
|
||||
// in production, this will be an hydration URL to the selected file's data
|
||||
file: FILE_URL,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await appTester(
|
||||
App.creates.uploadFile_v10.operation.perform,
|
||||
bundle
|
||||
);
|
||||
expect(result.filename).toBe('sample.pdf');
|
||||
expect(result.file.sha1).toBe(EXPECTED_SHA1);
|
||||
});
|
||||
|
||||
test('upload file v9', async () => {
|
||||
const bundle = {
|
||||
inputData: {
|
||||
filename: 'sample.pdf',
|
||||
|
||||
// in production, this will be an hydration URL to the selected file's data
|
||||
file: FILE_URL,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await appTester(
|
||||
App.creates.uploadFile_v9.operation.perform,
|
||||
bundle
|
||||
);
|
||||
expect(result.filename).toBe('sample.pdf');
|
||||
expect(result.file.sha1).toBe(EXPECTED_SHA1);
|
||||
});
|
||||
});
|
||||
27
vendor/zapier-platform/packages/cli/src/generators/templates/files/test/hydrators.test.js
vendored
Normal file
27
vendor/zapier-platform/packages/cli/src/generators/templates/files/test/hydrators.test.js
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/* globals describe, expect, test */
|
||||
|
||||
const zapier = require('zapier-platform-core');
|
||||
|
||||
const App = require('../index');
|
||||
const appTester = zapier.createAppTester(App);
|
||||
zapier.tools.env.inject();
|
||||
|
||||
describe('downloadFile', () => {
|
||||
test('download file', async () => {
|
||||
if (!process.env.ZAPIER_DEPLOY_KEY) {
|
||||
console.warn('skipped as ZAPIER_DEPLOY_KEY is not defined');
|
||||
return;
|
||||
}
|
||||
|
||||
const bundle = {
|
||||
inputData: {
|
||||
url: 'https://httpbin.zapier-tooling.com/xml',
|
||||
},
|
||||
};
|
||||
|
||||
const url = await appTester(App.hydrators.downloadFile, bundle);
|
||||
expect(url).toContain(
|
||||
'https://zapier-dev-files.s3.amazonaws.com/cli-platform/'
|
||||
);
|
||||
});
|
||||
});
|
||||
26
vendor/zapier-platform/packages/cli/src/generators/templates/files/test/triggers.test.js
vendored
Normal file
26
vendor/zapier-platform/packages/cli/src/generators/templates/files/test/triggers.test.js
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/* globals describe, expect, test */
|
||||
|
||||
const zapier = require('zapier-platform-core');
|
||||
|
||||
const App = require('../index');
|
||||
const appTester = zapier.createAppTester(App);
|
||||
zapier.tools.env.inject();
|
||||
|
||||
describe('newFile', () => {
|
||||
test('fetch files', async () => {
|
||||
const bundle = {};
|
||||
const results = await appTester(
|
||||
App.triggers.newFile.operation.perform,
|
||||
bundle
|
||||
);
|
||||
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
|
||||
// The 'hydrate|||' thing how Zapier represents dehydrated data
|
||||
const firstFile = results[0];
|
||||
expect(firstFile).toEqual({
|
||||
id: expect.stringMatching(/^https:/),
|
||||
file: expect.stringMatching(/^hydrate\|\|\|/),
|
||||
});
|
||||
});
|
||||
});
|
||||
46
vendor/zapier-platform/packages/cli/src/generators/templates/files/triggers/newFile.js
vendored
Normal file
46
vendor/zapier-platform/packages/cli/src/generators/templates/files/triggers/newFile.js
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
const hydrators = require('../hydrators');
|
||||
|
||||
const perform = (z, bundle) => {
|
||||
// In reality you're more likely to get file info from a remote server. Here
|
||||
// we're hard coding some links just to demonstrate.
|
||||
const fileURLs = [
|
||||
'https://httpbin.zapier-tooling.com/image/png',
|
||||
'https://httpbin.zapier-tooling.com/image/jpeg',
|
||||
'https://httpbin.zapier-tooling.com/xml',
|
||||
];
|
||||
|
||||
return fileURLs.map((fileURL) => {
|
||||
const fileInfo = {
|
||||
id: fileURL,
|
||||
|
||||
// Make it possible to get the actual file contents if necessary. No need
|
||||
// to make the request to download files now when the trigger is run.
|
||||
file: z.dehydrateFile(hydrators.downloadFile, { url: fileURL }),
|
||||
};
|
||||
return fileInfo;
|
||||
});
|
||||
};
|
||||
|
||||
// We recommend writing your triggers separate like this and rolling them into
|
||||
// the App definition at the end.
|
||||
module.exports = {
|
||||
key: 'newFile',
|
||||
|
||||
// You'll want to provide some helpful display labels and descriptions
|
||||
// for users. Zapier will put them into the UX.
|
||||
noun: 'File',
|
||||
display: {
|
||||
label: 'New File',
|
||||
description: 'Triggers when a new file is added.',
|
||||
},
|
||||
|
||||
// `operation` is where the business logic goes.
|
||||
operation: {
|
||||
perform,
|
||||
|
||||
sample: {
|
||||
id: 'https://example.com/file.txt',
|
||||
file: 'content',
|
||||
},
|
||||
},
|
||||
};
|
||||
66
vendor/zapier-platform/packages/cli/src/generators/templates/gitignore
vendored
Normal file
66
vendor/zapier-platform/packages/cli/src/generators/templates/gitignore
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/
|
||||
dist/
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# environment variables file
|
||||
.env
|
||||
.environment
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
.pnpm-store/
|
||||
36
vendor/zapier-platform/packages/cli/src/generators/templates/index-esm.template.js
vendored
Normal file
36
vendor/zapier-platform/packages/cli/src/generators/templates/index-esm.template.js
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<% if (hasAuth) { %>
|
||||
import {
|
||||
config as authentication,
|
||||
befores = [],
|
||||
afters = [],
|
||||
} from './authentication';
|
||||
<% } %>
|
||||
|
||||
import packageJson from './package.json' with { type: 'json' };
|
||||
import zapier from '<%= corePackageName %>';
|
||||
|
||||
export default {
|
||||
// This is just shorthand to reference the installed dependencies you have.
|
||||
// Zapier will need to know these before we can upload.
|
||||
version: packageJson.version,
|
||||
platformVersion: zapier.version,
|
||||
|
||||
<% if (hasAuth) { %>
|
||||
authentication,
|
||||
|
||||
beforeRequest: [...befores],
|
||||
|
||||
afterResponse: [...afters],
|
||||
<% } %>
|
||||
|
||||
// If you want your trigger to show up, you better include it here!
|
||||
triggers: {},
|
||||
|
||||
// If you want your searches to show up, you better include it here!
|
||||
searches: {},
|
||||
|
||||
// If you want your creates to show up, you better include it here!
|
||||
creates: {},
|
||||
|
||||
resources: {},
|
||||
};
|
||||
30
vendor/zapier-platform/packages/cli/src/generators/templates/index.template.js
vendored
Normal file
30
vendor/zapier-platform/packages/cli/src/generators/templates/index.template.js
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<% if (hasAuth) { %>
|
||||
const authentication = require('./authentication');
|
||||
const { befores = [], afters = [] } = require('./middleware');
|
||||
<% } %>
|
||||
|
||||
module.exports = {
|
||||
// This is just shorthand to reference the installed dependencies you have.
|
||||
// Zapier will need to know these before we can upload.
|
||||
version: require('./package.json').version,
|
||||
platformVersion: require('<%= corePackageName %>').version,
|
||||
|
||||
<% if (hasAuth) { %>
|
||||
authentication,
|
||||
|
||||
beforeRequest: [...befores],
|
||||
|
||||
afterResponse: [...afters],
|
||||
<% } %>
|
||||
|
||||
// If you want your trigger to show up, you better include it here!
|
||||
triggers: {},
|
||||
|
||||
// If you want your searches to show up, you better include it here!
|
||||
searches: {},
|
||||
|
||||
// If you want your creates to show up, you better include it here!
|
||||
creates: {},
|
||||
|
||||
resources: {},
|
||||
};
|
||||
21
vendor/zapier-platform/packages/cli/src/generators/templates/index.template.ts
vendored
Normal file
21
vendor/zapier-platform/packages/cli/src/generators/templates/index.template.ts
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import zapier, { defineApp } from 'zapier-platform-core';
|
||||
|
||||
import packageJson from '../package.json' with { type: 'json' };
|
||||
|
||||
import authentication from './authentication.js';
|
||||
import { befores, afters } from './middleware.js';
|
||||
|
||||
export default defineApp({
|
||||
version: packageJson.version,
|
||||
platformVersion: zapier.version,
|
||||
|
||||
authentication,
|
||||
beforeRequest: [...befores],
|
||||
afterResponse: [...afters],
|
||||
|
||||
// Add your triggers here for them to show up!
|
||||
triggers: {},
|
||||
|
||||
// Add your creates here for them to show up!
|
||||
creates: {},
|
||||
});
|
||||
16
vendor/zapier-platform/packages/cli/src/generators/templates/line-items/README.md
vendored
Normal file
16
vendor/zapier-platform/packages/cli/src/generators/templates/line-items/README.md
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# line-items
|
||||
|
||||
An example integration demonstrating line item support. Line items are fields
|
||||
with a `children` property that represent structured, repeating data — like rows
|
||||
in a spreadsheet or items in an order.
|
||||
|
||||
## Testing with `zapier-platform invoke`
|
||||
|
||||
```bash
|
||||
# Non-interactive with JSON input
|
||||
zapier-platform invoke create order --non-interactive \
|
||||
-i '{"name": "My Order", "line_items": [{"product_name": "Pens", "quantity": "12", "price": "1.50"}]}'
|
||||
|
||||
# Interactive mode — use the line item editing UI
|
||||
zapier-platform invoke create order -i '{"name": "My Order"}'
|
||||
```
|
||||
67
vendor/zapier-platform/packages/cli/src/generators/templates/line-items/creates/order.js
vendored
Normal file
67
vendor/zapier-platform/packages/cli/src/generators/templates/line-items/creates/order.js
vendored
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
const perform = async (z, bundle) => {
|
||||
const response = await z.request({
|
||||
url: 'https://httpbin.zapier-tooling.com/post',
|
||||
method: 'POST',
|
||||
body: {
|
||||
name: bundle.inputData.name,
|
||||
line_items: bundle.inputData.line_items,
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
key: 'order',
|
||||
noun: 'Order',
|
||||
display: {
|
||||
label: 'Create Order',
|
||||
description: 'Creates a new order with line items.',
|
||||
},
|
||||
operation: {
|
||||
inputFields: [
|
||||
{ key: 'name', required: true, type: 'string', label: 'Order Name' },
|
||||
{
|
||||
key: 'line_items',
|
||||
label: 'Line Items',
|
||||
children: [
|
||||
{
|
||||
key: 'product_name',
|
||||
type: 'string',
|
||||
label: 'Product Name',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'quantity',
|
||||
type: 'integer',
|
||||
label: 'Quantity',
|
||||
required: true,
|
||||
},
|
||||
{ key: 'price', type: 'number', label: 'Unit Price' },
|
||||
],
|
||||
},
|
||||
],
|
||||
perform,
|
||||
sample: {
|
||||
id: 1,
|
||||
name: 'Stationery Order',
|
||||
line_items: [
|
||||
{ product_name: 'Pens', quantity: 12, price: 1.5 },
|
||||
{ product_name: 'Notebooks', quantity: 3, price: 8.99 },
|
||||
],
|
||||
},
|
||||
outputFields: [
|
||||
{ key: 'id', label: 'ID' },
|
||||
{ key: 'name', label: 'Order Name' },
|
||||
{
|
||||
key: 'line_items',
|
||||
label: 'Line Items',
|
||||
children: [
|
||||
{ key: 'product_name', label: 'Product Name' },
|
||||
{ key: 'quantity', label: 'Quantity' },
|
||||
{ key: 'price', label: 'Unit Price' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
12
vendor/zapier-platform/packages/cli/src/generators/templates/line-items/index.js
vendored
Normal file
12
vendor/zapier-platform/packages/cli/src/generators/templates/line-items/index.js
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
const order = require('./creates/order');
|
||||
|
||||
const App = {
|
||||
version: require('./package.json').version,
|
||||
platformVersion: require('zapier-platform-core').version,
|
||||
|
||||
creates: {
|
||||
[order.key]: order,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = App;
|
||||
30
vendor/zapier-platform/packages/cli/src/generators/templates/line-items/test/creates.test.js
vendored
Normal file
30
vendor/zapier-platform/packages/cli/src/generators/templates/line-items/test/creates.test.js
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/* globals describe, expect, test */
|
||||
|
||||
const zapier = require('zapier-platform-core');
|
||||
|
||||
const App = require('../index');
|
||||
const appTester = zapier.createAppTester(App);
|
||||
zapier.tools.env.inject();
|
||||
|
||||
describe('creates', () => {
|
||||
test('create order with line items', async () => {
|
||||
const bundle = {
|
||||
inputData: {
|
||||
name: 'Test Order',
|
||||
line_items: [
|
||||
{ product_name: 'Pens', quantity: 12, price: 1.5 },
|
||||
{ product_name: 'Notebooks', quantity: 3, price: 8.99 },
|
||||
],
|
||||
},
|
||||
};
|
||||
const result = await appTester(
|
||||
App.creates.order.operation.perform,
|
||||
bundle,
|
||||
);
|
||||
const body = JSON.parse(result.data);
|
||||
expect(body.name).toBe('Test Order');
|
||||
expect(body.line_items).toHaveLength(2);
|
||||
expect(body.line_items[0].product_name).toBe('Pens');
|
||||
expect(body.line_items[1].quantity).toBe(3);
|
||||
});
|
||||
});
|
||||
3
vendor/zapier-platform/packages/cli/src/generators/templates/openai/README.md
vendored
Normal file
3
vendor/zapier-platform/packages/cli/src/generators/templates/openai/README.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# OpenAI
|
||||
|
||||
This Zapier integration project is generated by the `zapier-platform init` CLI command. This integration in particular is using the OpenAI API to generate responses to prompts from users. For this integration, there is a `constants.js` file that will allow you to swap out the base URL and version of the API to swap if you are using an OpenAI compatible API to get started.
|
||||
46
vendor/zapier-platform/packages/cli/src/generators/templates/openai/authentication.js
vendored
Normal file
46
vendor/zapier-platform/packages/cli/src/generators/templates/openai/authentication.js
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
const { API_URL } = require('./constants');
|
||||
// You want to make a request to an endpoint that is either specifically designed
|
||||
// to test auth, or one that every user will have access to. eg: `/me`.
|
||||
// By returning the entire request object, you have access to the request and
|
||||
// response data for testing purposes. Your connection label can access any data
|
||||
// from the returned response using the `json.` prefix. eg: `{{json.username}}`.
|
||||
const test = (z, bundle) => z.request({ url: `${API_URL}/me` });
|
||||
|
||||
module.exports = {
|
||||
// "custom" is the catch-all auth type. The user supplies some info and Zapier can
|
||||
// make authenticated requests with it
|
||||
type: 'custom',
|
||||
|
||||
// Define any input app's auth requires here. The user will be prompted to enter
|
||||
// this info when they connect their account.
|
||||
fields: [
|
||||
{
|
||||
key: 'api_key',
|
||||
label: 'API Key',
|
||||
required: true,
|
||||
helpText:
|
||||
'Generate an API Key in your [Platform settings page](https://platform.openai.com/api-keys).',
|
||||
},
|
||||
// This field is optional and can be removed if not needed
|
||||
{
|
||||
key: 'organization_id',
|
||||
required: false,
|
||||
label: 'Organization ID',
|
||||
helpText:
|
||||
'**Optional** Only required if your OpenAI account belongs to multiple organizations. If not using OpenAI, this field will be disregarded. If your OpenAI account belongs to multiple organizations, optionally add the [Organization ID](https://platform.openai.com/account/org-settings) that this connection should use. If left blank, your [default organization](https://platform.openai.com/account/api-keys) will be used.',
|
||||
},
|
||||
],
|
||||
|
||||
// The test method allows Zapier to verify that the credentials a user provides
|
||||
// are valid. We'll execute this method whenever a user connects their account for
|
||||
// the first time.
|
||||
test,
|
||||
|
||||
// This template string can access all the data returned from the auth test. If
|
||||
// you return the test object, you'll access the returned data with a label like
|
||||
// `{{json.X}}`. If you return `response.data` from your test, then your label can
|
||||
// be `{{X}}`. This can also be a function that returns a label. That function has
|
||||
// the standard args `(z, bundle)` and data returned from the test can be accessed
|
||||
// in `bundle.inputData.X`.
|
||||
connectionLabel: '{{json.email}}',
|
||||
};
|
||||
10
vendor/zapier-platform/packages/cli/src/generators/templates/openai/constants.js
vendored
Normal file
10
vendor/zapier-platform/packages/cli/src/generators/templates/openai/constants.js
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
const BASE_URL = 'https://api.openai.com';
|
||||
const VERSION = 'v1';
|
||||
const API_URL = `${BASE_URL}/${VERSION}`;
|
||||
|
||||
const DEFAULT_MODEL = 'gpt-4o-mini';
|
||||
|
||||
module.exports = {
|
||||
API_URL,
|
||||
DEFAULT_MODEL,
|
||||
};
|
||||
164
vendor/zapier-platform/packages/cli/src/generators/templates/openai/creates/chat_completion.js
vendored
Normal file
164
vendor/zapier-platform/packages/cli/src/generators/templates/openai/creates/chat_completion.js
vendored
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
/* eslint-disable camelcase */
|
||||
const { API_URL, DEFAULT_MODEL } = require('../constants');
|
||||
|
||||
const sample = require('../samples/chat.json');
|
||||
|
||||
async function getAdvancedFields(_z, bundle) {
|
||||
if (bundle.inputData.show_advanced === true) {
|
||||
return [
|
||||
{
|
||||
key: 'info_advanced',
|
||||
type: 'copy',
|
||||
helpText:
|
||||
"The following fields are for advanced users and should be used with caution as they may affect performance. In most cases, the default options are sufficient. If you'd like to explore these options further, you can [learn more here](https://help.zapier.com/hc/en-us/articles/22497191078797).",
|
||||
},
|
||||
{
|
||||
key: 'developer_message',
|
||||
label: 'Developer/System Message',
|
||||
type: 'text',
|
||||
helpText:
|
||||
'Instructions to the model that are prioritized ahead of user messages, following [chain of command](https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command).',
|
||||
},
|
||||
{
|
||||
key: 'temperature',
|
||||
label: 'Temperature',
|
||||
type: 'number',
|
||||
helpText:
|
||||
'Higher values mean the model will take more risks. Try 0.9 for more creative applications, and 0 for ones with a well-defined answer.\n\nUse a decimal between 0 and 1.',
|
||||
},
|
||||
{
|
||||
key: 'max_completion_tokens',
|
||||
label: 'Maximum Length',
|
||||
type: 'integer',
|
||||
helpText: 'The maximum number of tokens for the completion.',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async function perform(z, bundle) {
|
||||
const {
|
||||
user_message,
|
||||
model,
|
||||
files,
|
||||
developer_message,
|
||||
temperature,
|
||||
max_completion_tokens,
|
||||
} = bundle.inputData;
|
||||
|
||||
const developerMessage = {
|
||||
role: 'developer',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: developer_message || 'You are a helpful assistant.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const userMessage = {
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: user_message,
|
||||
},
|
||||
...(files
|
||||
? files.map((file) => ({
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: file,
|
||||
},
|
||||
}))
|
||||
: []),
|
||||
],
|
||||
};
|
||||
|
||||
const messages = [developerMessage, userMessage];
|
||||
|
||||
const response = await z.request({
|
||||
url: `${API_URL}/chat/completions`,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages,
|
||||
temperature,
|
||||
max_completion_tokens,
|
||||
}),
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
key: 'chat_completion',
|
||||
noun: 'Chat',
|
||||
display: {
|
||||
label: 'Chat Completion',
|
||||
description: 'Sends a Chat to OpenAI and generates a Completion.',
|
||||
},
|
||||
operation: {
|
||||
perform,
|
||||
inputFields: [
|
||||
{
|
||||
key: 'info_data_usage',
|
||||
type: 'copy',
|
||||
helpText:
|
||||
"Data sent to OpenAI through this Zap is via an API. Under OpenAI's [API data usage policy](https://openai.com/policies/api-data-usage-policies), OpenAI will not use API-submitted data to train or improve their models unless you explicitly decide to share your data with them for that purpose (such as by opting in). For more information, please review OpenAI's article about [when/how data may be used to improve model performance](https://help.openai.com/en/articles/5722486-how-your-data-is-used-to-improve-model-performance).",
|
||||
},
|
||||
{
|
||||
key: 'user_message',
|
||||
label: 'User Message',
|
||||
type: 'text',
|
||||
helpText:
|
||||
"Instructions that request some output from the model. Similar to messages you'd type in [ChatGPT](https://chatgpt.com) as an end user.",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'files',
|
||||
label: 'Images',
|
||||
type: 'file',
|
||||
helpText: 'Images to include along with your message.',
|
||||
list: true,
|
||||
},
|
||||
{
|
||||
key: 'model',
|
||||
label: 'Model',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: DEFAULT_MODEL, // Optional to default to a specific model for most users
|
||||
dynamic: 'list_models.id.name',
|
||||
altersDynamicFields: false,
|
||||
},
|
||||
{
|
||||
key: 'show_advanced',
|
||||
label: 'Show Advanced Options',
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
altersDynamicFields: true,
|
||||
},
|
||||
getAdvancedFields,
|
||||
],
|
||||
// Rename some of the output fields to be more descriptive for a user
|
||||
outputFields: [
|
||||
{ key: 'id', type: 'string', label: 'Completion ID' },
|
||||
{ key: 'model', type: 'string', label: 'Model' },
|
||||
{
|
||||
key: 'usage__prompt_tokens',
|
||||
type: 'number',
|
||||
label: 'Usage: Prompt Tokens',
|
||||
},
|
||||
{
|
||||
key: 'usage__completion_tokens',
|
||||
type: 'number',
|
||||
label: 'Usage: Completion Tokens',
|
||||
},
|
||||
{
|
||||
key: 'usage__total_tokens',
|
||||
type: 'number',
|
||||
label: 'Usage: Total Tokens',
|
||||
},
|
||||
],
|
||||
sample,
|
||||
},
|
||||
};
|
||||
7
vendor/zapier-platform/packages/cli/src/generators/templates/openai/creates/index.js
vendored
Normal file
7
vendor/zapier-platform/packages/cli/src/generators/templates/openai/creates/index.js
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/* eslint-disable camelcase */
|
||||
const chat_completion = require('./chat_completion');
|
||||
|
||||
// If you add a new create, make sure it is exported here to display in the Zapier Editor
|
||||
module.exports = {
|
||||
[chat_completion.key]: chat_completion,
|
||||
};
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
/* eslint-disable camelcase */
|
||||
const list_models = require('./list_models.js');
|
||||
|
||||
// If you add a new Dynamic Dropdown, make sure it is exported here to display in the Zapier Editor
|
||||
module.exports = {
|
||||
[list_models.key]: list_models,
|
||||
};
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
const { API_URL } = require('../constants');
|
||||
|
||||
const perform = async (z, bundle) => {
|
||||
const response = await z.request({ url: `${API_URL}/models` });
|
||||
|
||||
const responseData = response.data;
|
||||
|
||||
return responseData.data.map((model) => ({
|
||||
id: model.id,
|
||||
name: model.id,
|
||||
}));
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
key: 'list_models',
|
||||
noun: 'Model',
|
||||
display: {
|
||||
label: 'List of Models',
|
||||
description:
|
||||
'This is a hidden trigger, and is used in a Dynamic Dropdown of another trigger.',
|
||||
hidden: true,
|
||||
},
|
||||
operation: { perform },
|
||||
};
|
||||
33
vendor/zapier-platform/packages/cli/src/generators/templates/openai/index.js
vendored
Normal file
33
vendor/zapier-platform/packages/cli/src/generators/templates/openai/index.js
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/* eslint-disable camelcase */
|
||||
const authentication = require('./authentication');
|
||||
const middleware = require('./middleware');
|
||||
const dynamic_dropdowns = require('./dynamic_dropdowns');
|
||||
const creates = require('./creates');
|
||||
|
||||
module.exports = {
|
||||
// This is just shorthand to reference the installed dependencies you have.
|
||||
// Zapier will need to know these before we can upload.
|
||||
version: require('./package.json').version,
|
||||
platformVersion: require('zapier-platform-core').version,
|
||||
|
||||
authentication,
|
||||
|
||||
beforeRequest: [...middleware.befores],
|
||||
|
||||
afterResponse: [...middleware.afters],
|
||||
|
||||
// If you want your trigger to show up, you better include it here!
|
||||
triggers: {
|
||||
...dynamic_dropdowns,
|
||||
},
|
||||
|
||||
// If you want your searches to show up, you better include it here!
|
||||
searches: {},
|
||||
|
||||
// If you want your creates to show up, you better include it here!
|
||||
creates: {
|
||||
...creates,
|
||||
},
|
||||
|
||||
resources: {},
|
||||
};
|
||||
50
vendor/zapier-platform/packages/cli/src/generators/templates/openai/middleware.js
vendored
Normal file
50
vendor/zapier-platform/packages/cli/src/generators/templates/openai/middleware.js
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/* eslint-disable camelcase */
|
||||
// This function runs after every outbound request. You can use it to check for
|
||||
// errors or modify the response. You can have as many as you need. They'll need
|
||||
// to each be registered in your index.js file.
|
||||
const handleBadResponses = (response, z, bundle) => {
|
||||
if (response.data.error) {
|
||||
throw new z.errors.Error(
|
||||
response.data.error.message,
|
||||
response.data.error.code,
|
||||
response.status,
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
const includeOrgId = (request, z, bundle) => {
|
||||
const { organization_id } = bundle.authData;
|
||||
if (organization_id) {
|
||||
request.headers['OpenAI-Organization'] = organization_id;
|
||||
}
|
||||
return request;
|
||||
};
|
||||
|
||||
// This function runs before every outbound request. You can have as many as you
|
||||
// need. They'll need to each be registered in your index.js file.
|
||||
const includeApiKey = (request, z, bundle) => {
|
||||
const { api_key } = bundle.authData;
|
||||
if (api_key) {
|
||||
// Use these lines to include the API key in the querystring
|
||||
// request.params = request.params || {};
|
||||
// request.params.api_key = api_key;
|
||||
|
||||
// If you want to include the API key in the header:
|
||||
request.headers.Authorization = `Bearer ${api_key}`;
|
||||
}
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
const jsonHeaders = (request) => {
|
||||
request.headers['Content-Type'] = 'application/json';
|
||||
request.headers.Accept = 'application/json';
|
||||
return request;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
befores: [includeApiKey, includeOrgId, jsonHeaders],
|
||||
afters: [handleBadResponses],
|
||||
};
|
||||
29
vendor/zapier-platform/packages/cli/src/generators/templates/openai/samples/chat.json
vendored
Normal file
29
vendor/zapier-platform/packages/cli/src/generators/templates/openai/samples/chat.json
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"id": "chatcmpl-123",
|
||||
"object": "chat.completion",
|
||||
"created": 1677652288,
|
||||
"model": "gpt-4o-mini",
|
||||
"system_fingerprint": "fp_44709d6fcb",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "\n\nHello there, how may I assist you today?"
|
||||
},
|
||||
"logprobs": null,
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"service_tier": "default",
|
||||
"usage": {
|
||||
"prompt_tokens": 9,
|
||||
"completion_tokens": 12,
|
||||
"total_tokens": 21,
|
||||
"completion_tokens_details": {
|
||||
"reasoning_tokens": 0,
|
||||
"accepted_prediction_tokens": 0,
|
||||
"rejected_prediction_tokens": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
66
vendor/zapier-platform/packages/cli/src/generators/templates/openai/test/authentication.test.js
vendored
Normal file
66
vendor/zapier-platform/packages/cli/src/generators/templates/openai/test/authentication.test.js
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/* globals describe, it, expect */
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
const App = require('../index');
|
||||
|
||||
describe('custom auth', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('passes authentication and returns json', async () => {
|
||||
const bundle = {
|
||||
authData: {
|
||||
api_key: 'secret',
|
||||
},
|
||||
};
|
||||
|
||||
// Mock successful response
|
||||
const mockResponse = {
|
||||
status: 200,
|
||||
data: {
|
||||
email: 'test@example.com',
|
||||
},
|
||||
};
|
||||
|
||||
// Mock the request client
|
||||
const mockRequest = jest.fn().mockResolvedValue(mockResponse);
|
||||
const z = {
|
||||
request: mockRequest,
|
||||
};
|
||||
|
||||
const response = await App.authentication.test(z, bundle);
|
||||
|
||||
expect(mockRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: expect.stringContaining('/me'),
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.data).toHaveProperty('email', 'test@example.com');
|
||||
});
|
||||
|
||||
it('fails on bad auth', async () => {
|
||||
const bundle = {
|
||||
authData: {
|
||||
api_key: 'bad',
|
||||
},
|
||||
};
|
||||
|
||||
// Mock failed response
|
||||
const mockRequest = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Incorrect API key provided'));
|
||||
const z = {
|
||||
request: mockRequest,
|
||||
};
|
||||
|
||||
try {
|
||||
await App.authentication.test(z, bundle);
|
||||
} catch (error) {
|
||||
expect(mockRequest).toHaveBeenCalledTimes(1);
|
||||
expect(error.message).toContain('Incorrect API key provided');
|
||||
return;
|
||||
}
|
||||
throw new Error('appTester should have thrown');
|
||||
});
|
||||
});
|
||||
150
vendor/zapier-platform/packages/cli/src/generators/templates/openai/test/chat_completion.test.js
vendored
Normal file
150
vendor/zapier-platform/packages/cli/src/generators/templates/openai/test/chat_completion.test.js
vendored
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
/* globals describe, it, expect */
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
const chatCompletion = require('../creates/chat_completion');
|
||||
const { DEFAULT_MODEL } = require('../constants');
|
||||
|
||||
describe('chat_completion', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('creates a basic chat completion', async () => {
|
||||
const bundle = {
|
||||
inputData: {
|
||||
user_message: 'Hello, how are you?',
|
||||
model: DEFAULT_MODEL,
|
||||
},
|
||||
};
|
||||
|
||||
const mockResponse = {
|
||||
data: {
|
||||
id: 'chatcmpl-123',
|
||||
model: DEFAULT_MODEL,
|
||||
usage: {
|
||||
prompt_tokens: 20,
|
||||
completion_tokens: 15,
|
||||
total_tokens: 35,
|
||||
},
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: 'I am doing well, thank you for asking!',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const mockRequest = jest.fn().mockResolvedValue(mockResponse);
|
||||
const z = { request: mockRequest };
|
||||
|
||||
const result = await chatCompletion.operation.perform(z, bundle);
|
||||
|
||||
expect(mockRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: expect.stringContaining('/chat/completions'),
|
||||
method: 'POST',
|
||||
body: expect.stringContaining(bundle.inputData.user_message),
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
it('creates a chat completion with advanced options', async () => {
|
||||
const bundle = {
|
||||
inputData: {
|
||||
user_message: 'Write a story',
|
||||
model: DEFAULT_MODEL,
|
||||
developer_message: 'You are a creative writer',
|
||||
temperature: 0.9,
|
||||
max_completion_tokens: 100,
|
||||
},
|
||||
};
|
||||
|
||||
const mockResponse = {
|
||||
data: {
|
||||
id: 'chatcmpl-456',
|
||||
model: DEFAULT_MODEL,
|
||||
usage: {
|
||||
prompt_tokens: 25,
|
||||
completion_tokens: 50,
|
||||
total_tokens: 75,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockRequest = jest.fn().mockResolvedValue(mockResponse);
|
||||
const z = { request: mockRequest };
|
||||
|
||||
const result = await chatCompletion.operation.perform(z, bundle);
|
||||
|
||||
expect(mockRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: expect.stringContaining('/chat/completions'),
|
||||
method: 'POST',
|
||||
body: expect.stringMatching(/temperature.*0.9/),
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
it('creates a chat completion with image', async () => {
|
||||
const bundle = {
|
||||
inputData: {
|
||||
user_message: 'Describe this image',
|
||||
model: DEFAULT_MODEL,
|
||||
files: ['https://example.com/image.jpg'],
|
||||
},
|
||||
};
|
||||
|
||||
const mockResponse = {
|
||||
data: {
|
||||
id: 'chatcmpl-789',
|
||||
model: DEFAULT_MODEL,
|
||||
usage: {
|
||||
prompt_tokens: 30,
|
||||
completion_tokens: 20,
|
||||
total_tokens: 50,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockRequest = jest.fn().mockResolvedValue(mockResponse);
|
||||
const z = { request: mockRequest };
|
||||
|
||||
const result = await chatCompletion.operation.perform(z, bundle);
|
||||
|
||||
expect(mockRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: expect.stringContaining('/chat/completions'),
|
||||
method: 'POST',
|
||||
body: expect.stringMatching(/image_url.*example.com/),
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
it('handles API errors', async () => {
|
||||
const bundle = {
|
||||
inputData: {
|
||||
user_message: 'Hello',
|
||||
model: DEFAULT_MODEL,
|
||||
},
|
||||
};
|
||||
|
||||
const mockRequest = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Invalid request'));
|
||||
const z = { request: mockRequest };
|
||||
|
||||
try {
|
||||
await chatCompletion.operation.perform(z, bundle);
|
||||
} catch (error) {
|
||||
expect(mockRequest).toHaveBeenCalledTimes(1);
|
||||
expect(error.message).toContain('Invalid request');
|
||||
return;
|
||||
}
|
||||
throw new Error('Should have thrown an error');
|
||||
});
|
||||
});
|
||||
51
vendor/zapier-platform/packages/cli/src/generators/templates/openai/test/list_models.test.js
vendored
Normal file
51
vendor/zapier-platform/packages/cli/src/generators/templates/openai/test/list_models.test.js
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/* globals describe, it, expect */
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
const listModels = require('../dynamic_dropdowns/list_models');
|
||||
|
||||
describe('list_models', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns formatted list of models', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
data: [{ id: 'gpt-4' }, { id: 'gpt-3.5-turbo' }],
|
||||
},
|
||||
};
|
||||
|
||||
const mockRequest = jest.fn().mockResolvedValue(mockResponse);
|
||||
const z = { request: mockRequest };
|
||||
const bundle = {};
|
||||
|
||||
const results = await listModels.operation.perform(z, bundle);
|
||||
|
||||
expect(mockRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: expect.stringContaining('/models'),
|
||||
});
|
||||
|
||||
expect(results).toEqual([
|
||||
{ id: 'gpt-4', name: 'gpt-4' },
|
||||
{ id: 'gpt-3.5-turbo', name: 'gpt-3.5-turbo' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles API errors', async () => {
|
||||
const mockRequest = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Failed to fetch models'));
|
||||
const z = { request: mockRequest };
|
||||
const bundle = {};
|
||||
|
||||
try {
|
||||
await listModels.operation.perform(z, bundle);
|
||||
} catch (error) {
|
||||
expect(mockRequest).toHaveBeenCalledTimes(1);
|
||||
expect(error.message).toContain('Failed to fetch models');
|
||||
return;
|
||||
}
|
||||
throw new Error('Should have thrown an error');
|
||||
});
|
||||
});
|
||||
6
vendor/zapier-platform/packages/cli/src/generators/templates/search-or-create/.gitignore
vendored
Normal file
6
vendor/zapier-platform/packages/cli/src/generators/templates/search-or-create/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
build
|
||||
docs
|
||||
node_modules
|
||||
*.log
|
||||
.environment
|
||||
lib.env
|
||||
5
vendor/zapier-platform/packages/cli/src/generators/templates/search-or-create/README.md
vendored
Normal file
5
vendor/zapier-platform/packages/cli/src/generators/templates/search-or-create/README.md
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# The "search-or-create" Template
|
||||
|
||||
An example showcasing a Search-or-Create.
|
||||
|
||||

|
||||
34
vendor/zapier-platform/packages/cli/src/generators/templates/search-or-create/creates/recipe.js
vendored
Normal file
34
vendor/zapier-platform/packages/cli/src/generators/templates/search-or-create/creates/recipe.js
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
const perform = async (z, bundle) => {
|
||||
const response = await z.request({
|
||||
method: 'POST',
|
||||
url: 'https://auth-json-server.zapier-staging.com/recipes',
|
||||
body: {
|
||||
name: bundle.inputData.name,
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
key: 'recipe',
|
||||
noun: 'Recipe',
|
||||
|
||||
display: {
|
||||
label: 'Create Recipe',
|
||||
description: 'Creates a recipe.',
|
||||
},
|
||||
|
||||
operation: {
|
||||
inputFields: [
|
||||
{ key: 'name', required: true },
|
||||
{ key: 'directions', required: false },
|
||||
{ key: 'style', required: false },
|
||||
],
|
||||
perform,
|
||||
|
||||
sample: {
|
||||
id: 1,
|
||||
name: 'Test',
|
||||
},
|
||||
},
|
||||
};
|
||||
35
vendor/zapier-platform/packages/cli/src/generators/templates/search-or-create/index.js
vendored
Normal file
35
vendor/zapier-platform/packages/cli/src/generators/templates/search-or-create/index.js
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
const RecipeCreate = require('./creates/recipe');
|
||||
const RecipeSearch = require('./searches/recipe');
|
||||
|
||||
const addAuthHeader = (request, z, bundle) => {
|
||||
// Hard-coded auth header just for demo. DON'T do auth like this for your
|
||||
// production app!
|
||||
request.headers['X-Api-Key'] = 'secret';
|
||||
return request;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
version: require('./package.json').version,
|
||||
platformVersion: require('zapier-platform-core').version,
|
||||
|
||||
beforeRequest: [addAuthHeader],
|
||||
|
||||
searches: { [RecipeSearch.key]: RecipeSearch },
|
||||
|
||||
creates: { [RecipeCreate.key]: RecipeCreate },
|
||||
|
||||
searchOrCreates: {
|
||||
[RecipeSearch.key]: {
|
||||
// The key must match the search
|
||||
key: RecipeSearch.key, // same as above
|
||||
display: {
|
||||
// The label shows up when the search-or-create checkbox is checked.
|
||||
// See https://cdn.zappy.app/5fc31d104c6bd0050c44510557b3b98f.png
|
||||
label: 'Find or Create a Recipe',
|
||||
description: 'x', // this is ignored
|
||||
},
|
||||
search: RecipeSearch.key,
|
||||
create: RecipeCreate.key,
|
||||
},
|
||||
},
|
||||
};
|
||||
35
vendor/zapier-platform/packages/cli/src/generators/templates/search-or-create/searches/recipe.js
vendored
Normal file
35
vendor/zapier-platform/packages/cli/src/generators/templates/search-or-create/searches/recipe.js
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
const perform = async (z, bundle) => {
|
||||
const response = await z.request({
|
||||
url: 'https://auth-json-server.zapier-staging.com/recipes',
|
||||
params: {
|
||||
name: bundle.inputData.name,
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
key: 'recipe',
|
||||
noun: 'Recipe',
|
||||
|
||||
display: {
|
||||
label: 'Find Recipe',
|
||||
description: 'Finds a recipe.',
|
||||
},
|
||||
|
||||
operation: {
|
||||
inputFields: [
|
||||
{
|
||||
key: 'name',
|
||||
required: true,
|
||||
helpText: 'Find the Recipe with this name.',
|
||||
},
|
||||
],
|
||||
perform,
|
||||
|
||||
sample: {
|
||||
id: 1,
|
||||
name: 'Test',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/* globals describe, expect, test */
|
||||
|
||||
const zapier = require('zapier-platform-core');
|
||||
|
||||
const App = require('../index');
|
||||
const appTester = zapier.createAppTester(App);
|
||||
zapier.tools.env.inject();
|
||||
|
||||
describe('recipe', () => {
|
||||
test('create with a name', async () => {
|
||||
const bundle = { inputData: { name: 'Pancake' } };
|
||||
const result = await appTester(
|
||||
App.creates.recipe.operation.perform,
|
||||
bundle
|
||||
);
|
||||
expect(result.id).toBeTruthy();
|
||||
expect(result.name).toBe('Pancake');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/* globals describe, expect, test */
|
||||
|
||||
const zapier = require('zapier-platform-core');
|
||||
|
||||
const App = require('../index');
|
||||
const appTester = zapier.createAppTester(App);
|
||||
zapier.tools.env.inject();
|
||||
|
||||
describe('recipe', () => {
|
||||
test('search by name', async () => {
|
||||
const bundle = { inputData: { name: 'name 1' } };
|
||||
const results = await appTester(
|
||||
App.searches.recipe.operation.perform,
|
||||
bundle
|
||||
);
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
|
||||
const firstRecipe = results[0];
|
||||
expect(firstRecipe).toMatchObject({
|
||||
id: '1',
|
||||
name: 'name 1',
|
||||
});
|
||||
});
|
||||
});
|
||||
18
vendor/zapier-platform/packages/cli/src/generators/templates/tsconfig.template.json
vendored
Normal file
18
vendor/zapier-platform/packages/cli/src/generators/templates/tsconfig.template.json
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"isolatedModules": true,
|
||||
"noImplicitAny": false,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true
|
||||
},
|
||||
"include": ["./src/**/*.ts"],
|
||||
"exclude": ["./**/*.test.ts"]
|
||||
}
|
||||
3
vendor/zapier-platform/packages/cli/src/index.js
vendored
Normal file
3
vendor/zapier-platform/packages/cli/src/index.js
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
// for now, requiring this file has no real effect
|
||||
|
||||
module.exports = require('@oclif/core');
|
||||
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,
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue