zapier-platform-cli 12.2.1 → 14.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,72 +5,37 @@ const { callAPI } = require('../../utils/api');
5
5
  const { convertApp } = require('../../utils/convert');
6
6
  const { isExistingEmptyDir } = require('../../utils/files');
7
7
  const { initApp } = require('../../utils/init');
8
- const { BASE_ENDPOINT } = require('../../constants');
9
8
 
10
9
  const { flags } = require('@oclif/command');
11
10
 
12
11
  class ConvertCommand extends BaseCommand {
13
- generateCreateFunc(isVisual, appId, version) {
12
+ generateCreateFunc(appId, version) {
14
13
  return async (tempAppDir) => {
15
- if (isVisual) {
16
- // has info about the app, such as title
17
- // has a CLI version of the actual app implementation
18
- this.throwForInvalidVersion(version);
19
- this.startSpinner('Downloading integration from Zapier');
20
- try {
21
- const [appInfo, versionInfo] = await Promise.all([
22
- callAPI(`/apps/${appId}`, undefined, true),
23
- callAPI(`/apps/${appId}/versions/${version}`, undefined, true),
24
- ]);
25
-
26
- if (!versionInfo.definition_override) {
27
- this.error(
28
- `Integration ${appId} @ ${version} is already a CLI integration and can't be converted. Instead, pick a version that was created using the Visual Builder.`
29
- );
30
- }
31
- this.stopSpinner();
32
-
33
- return convertApp(
34
- appInfo,
35
- versionInfo.definition_override,
36
- tempAppDir
14
+ // has info about the app, such as title
15
+ // has a CLI version of the actual app implementation
16
+ this.throwForInvalidVersion(version);
17
+ this.startSpinner('Downloading integration from Zapier');
18
+ try {
19
+ const [appInfo, versionInfo] = await Promise.all([
20
+ callAPI(`/apps/${appId}`, undefined, true),
21
+ callAPI(`/apps/${appId}/versions/${version}`, undefined, true),
22
+ ]);
23
+
24
+ if (!versionInfo.definition_override) {
25
+ this.error(
26
+ `Integration ${appId} @ ${version} is already a CLI integration and can't be converted. Instead, pick a version that was created using the Visual Builder.`
37
27
  );
38
- } catch (e) {
39
- if (e.status === 404) {
40
- this.error(
41
- `Visual Builder integration ${appId} @ ${version} not found. If you want to convert a Legacy Web Builder app, don't pass a \`--version\` option. Otherwise, double check the integration id and version.`
42
- );
43
- }
44
- this.error(e);
45
28
  }
46
- } else {
47
- // has info about the app, such as title
48
- const legacyDumpUrl = `${BASE_ENDPOINT}/api/developer/v1/apps/${appId}/dump`;
49
- // has a CLI version of the actual app implementation
50
- const cliDumpUrl = `${BASE_ENDPOINT}/api/developer/v1/apps/${appId}/cli-dump`;
51
-
52
- this.startSpinner('Downloading integration from Zapier');
53
-
54
- try {
55
- const [legacyApp, appDefinition] = await Promise.all([
56
- // these have weird call signatures because we're not calling the platform api
57
- callAPI(null, { url: legacyDumpUrl }, true),
58
- callAPI(null, { url: cliDumpUrl }, true),
59
- ]);
60
- // The JSON dump of the app doesn't have app ID, let's add it here
61
- legacyApp.general.app_id = appId;
29
+ this.stopSpinner();
62
30
 
63
- this.stopSpinner();
64
-
65
- return convertApp(legacyApp, appDefinition, tempAppDir);
66
- } catch (e) {
67
- if (e.status === 404) {
68
- this.error(
69
- `Legacy Web Builder app ${appId} not found. If you want to convert a Visual Builder integration, make sure to pass a \`--version\` option.`
70
- );
71
- }
72
- this.error(e);
31
+ return convertApp(appInfo, versionInfo.definition_override, tempAppDir);
32
+ } catch (e) {
33
+ if (e.status === 404) {
34
+ this.error(
35
+ `Visual Builder integration ${appId} @ ${version} not found. Double check the integration id and version.`
36
+ );
73
37
  }
38
+ this.error(e.json.errors[0]);
74
39
  }
75
40
  };
76
41
  }
@@ -83,7 +48,6 @@ class ConvertCommand extends BaseCommand {
83
48
  'You must provide an integrationId. See zapier convert --help for more info.'
84
49
  );
85
50
  }
86
- const isVisual = Boolean(this.flags.version);
87
51
 
88
52
  if (
89
53
  (await isExistingEmptyDir(path)) &&
@@ -92,7 +56,7 @@ class ConvertCommand extends BaseCommand {
92
56
  this.exit();
93
57
  }
94
58
 
95
- await initApp(path, this.generateCreateFunc(isVisual, appId, version));
59
+ await initApp(path, this.generateCreateFunc(appId, version));
96
60
  }
97
61
  }
98
62
 
@@ -100,7 +64,7 @@ ConvertCommand.args = [
100
64
  {
101
65
  name: 'integrationId',
102
66
  required: true,
103
- description: `To get the integration/app ID, go to "${BASE_ENDPOINT}/app/developer", click on an integration, and copy the number directly after "/app/" in the URL.`,
67
+ 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.`,
104
68
  parse: (input) => Number(input),
105
69
  },
106
70
  {
@@ -116,14 +80,13 @@ ConvertCommand.flags = buildFlags({
116
80
  char: 'v',
117
81
  description:
118
82
  'Convert a specific version. Required when converting a Visual Builder integration.',
83
+ required: true,
119
84
  }),
120
85
  },
121
86
  });
122
- ConvertCommand.description = `Convert a Legacy Web Builder app or Visual Builder integration to a CLI integration.
123
-
124
- If you're converting a **Legacy Web Builder** app: the new integration will have a dependency named zapier-platform-legacy-scripting-runner, a shim used to simulate behaviors that are specific to Legacy Web Builder. There could be differences on how the shim simulates and how Legacy Web Builder actually behaves on some edge cases, especially you have custom scripting code.
87
+ ConvertCommand.description = `Convert a Visual Builder integration to a CLI integration.
125
88
 
126
- If you're converting a **Visual Builder** app, then it will be identical and ready to push and use immediately!
89
+ The resulting CLI integration will be identical to its Visual Builder version and ready to push and use immediately!
127
90
 
128
91
  If you re-run this command on an existing directory it will leave existing files alone and not clobber them.
129
92
 
@@ -31,7 +31,7 @@ class LinkCommand extends BaseCommand {
31
31
  })),
32
32
  (app) => app.name.toLowerCase()
33
33
  ),
34
- 15
34
+ { pageSize: 15 }
35
35
  );
36
36
 
37
37
  this.startSpinner(`Setting up ${CURRENT_APP_FILE}`);
@@ -7,6 +7,7 @@ const { buildFlags } = require('../buildFlags');
7
7
  const { callAPI } = require('../../utils/api');
8
8
  const { flattenCheckResult } = require('../../utils/display');
9
9
  const { getVersionChangelog } = require('../../utils/changelog');
10
+ const checkMissingAppInfo = require('../../utils/check-missing-app-info');
10
11
 
11
12
  const serializeErrors = (errors) => {
12
13
  const opener = 'Promotion failed for the following reasons:\n\n';
@@ -15,7 +16,7 @@ const serializeErrors = (errors) => {
15
16
  return opener + errors.map((e) => `* ${e}`).join('\n');
16
17
  }
17
18
 
18
- const issues = flattenCheckResult({ errors: errors });
19
+ const issues = flattenCheckResult({ errors });
19
20
  return (
20
21
  opener +
21
22
  issues
@@ -28,6 +29,8 @@ class PromoteCommand extends BaseCommand {
28
29
  async perform() {
29
30
  const app = await this.getWritableApp();
30
31
 
32
+ checkMissingAppInfo(app);
33
+
31
34
  const version = this.args.version;
32
35
  const assumeYes = 'yes' in this.flags;
33
36
 
@@ -1,26 +1,238 @@
1
+ const colors = require('colors/safe');
2
+ const { flags } = require('@oclif/command');
3
+
1
4
  const ZapierBaseCommand = require('../ZapierBaseCommand');
2
- const { CURRENT_APP_FILE } = require('../../constants');
5
+ const { CURRENT_APP_FILE, MAX_DESCRIPTION_LENGTH } = require('../../constants');
3
6
  const { buildFlags } = require('../buildFlags');
4
- const { callAPI, writeLinkedAppConfig } = require('../../utils/api');
7
+ const {
8
+ callAPI,
9
+ getLinkedAppConfig,
10
+ getWritableApp,
11
+ isPublished,
12
+ writeLinkedAppConfig,
13
+ } = require('../../utils/api');
5
14
 
6
15
  class RegisterCommand extends ZapierBaseCommand {
16
+ /**
17
+ * Entry point function that runs when user runs `zapier register`
18
+ */
7
19
  async perform() {
8
- let title = this.args.title;
9
- if (!title) {
10
- title = await this.prompt('What is the title of your integration?');
11
- }
12
-
13
- this.startSpinner(`Registering your new integration "${title}"`);
14
- const app = await callAPI('/apps', { method: 'POST', body: { title } });
15
- this.stopSpinner();
16
- this.startSpinner(
17
- `Linking app to current directory with \`${CURRENT_APP_FILE}\``
18
- );
19
- await writeLinkedAppConfig(app, process.cwd());
20
- this.stopSpinner();
21
- this.log(
22
- '\nFinished! Now that your integration is registered with Zapier, you can `zapier push`!'
23
- );
20
+ // Flag validation
21
+ this._validateEnumFlags();
22
+ if (
23
+ 'desc' in this.flags &&
24
+ this.flags.desc.length > MAX_DESCRIPTION_LENGTH
25
+ ) {
26
+ throw new Error(
27
+ `Please provide a description that is ${MAX_DESCRIPTION_LENGTH} characters or less.`
28
+ );
29
+ }
30
+
31
+ const { appMeta, action } = await this._promptForAppMeta();
32
+
33
+ switch (action) {
34
+ case 'update': {
35
+ this.startSpinner(
36
+ `Updating your existing integration "${appMeta.title}"`
37
+ );
38
+ await callAPI(`/apps/${this.app.id}`, {
39
+ method: 'PUT',
40
+ body: appMeta,
41
+ });
42
+ this.stopSpinner();
43
+ this.log('\nIntegration successfully updated!');
44
+ break;
45
+ }
46
+
47
+ case 'register': {
48
+ this.startSpinner(
49
+ `Registering your new integration "${appMeta.title}"`
50
+ );
51
+ const app = await callAPI('/apps?formId=create', {
52
+ method: 'POST',
53
+ body: appMeta,
54
+ });
55
+ this.stopSpinner();
56
+ this.startSpinner(
57
+ `Linking app to current directory with \`${CURRENT_APP_FILE}\``
58
+ );
59
+ await writeLinkedAppConfig(app, process.cwd());
60
+ this.stopSpinner();
61
+ this.log(
62
+ '\nFinished! Now that your integration is registered with Zapier, you can `zapier push`!'
63
+ );
64
+ break;
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Validates values provided for enum flags against options retrieved from the BE
71
+ * (see getAppRegistrationFieldChoices hook for more details)
72
+ */
73
+ _validateEnumFlags() {
74
+ const flagFieldMappings = {
75
+ audience: 'intention',
76
+ role: 'role',
77
+ category: 'app_category',
78
+ };
79
+
80
+ for (const [flag, flagValue] of Object.entries(this.flags)) {
81
+ // Only validate user input for enum flags (in flagFieldMappings)
82
+ if (!flagFieldMappings[flag]) {
83
+ continue;
84
+ }
85
+
86
+ // Check user input against this.config.enumFieldChoices (retrieved in getAppRegistrationFieldChoices hook)
87
+ const enumFieldChoices =
88
+ this.config.enumFieldChoices[flagFieldMappings[flag]];
89
+ if (!enumFieldChoices.find((option) => option.value === flagValue)) {
90
+ throw new Error(
91
+ `${flagValue} is not a valid value for ${flag}. Must be one of the following: ${enumFieldChoices
92
+ .map((option) => option.value)
93
+ .join(', ')}`
94
+ );
95
+ }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Prompts user for values that have not been provided
101
+ * Flags can heavily impact the behavior of this function
102
+ * @returns { appMeta: {object}, action: string }
103
+ */
104
+ async _promptForAppMeta() {
105
+ const appMeta = {};
106
+
107
+ const actionChoices = [
108
+ { name: 'Yes, update current integration', value: 'update' },
109
+ { name: 'No, register a new integration', value: 'register' },
110
+ ];
111
+
112
+ let action = actionChoices[1].value; // Default action is register
113
+
114
+ const linkedAppId = (await getLinkedAppConfig(undefined, false))?.id;
115
+ if (linkedAppId) {
116
+ console.info(colors.yellow(`${CURRENT_APP_FILE} file detected.`));
117
+ if (this.flags.yes) {
118
+ console.info(
119
+ colors.yellow(
120
+ `-y/--yes flag passed, updating current integration (ID: ${linkedAppId}).`
121
+ )
122
+ );
123
+ action = actionChoices[0].value;
124
+ } else {
125
+ action = await this.promptWithList(
126
+ `Would you like to update your current integration (ID: ${linkedAppId})?`,
127
+ actionChoices
128
+ );
129
+ }
130
+ }
131
+
132
+ if (action === 'update') {
133
+ this.startSpinner('Retrieving details for your integration');
134
+ this.app = await getWritableApp();
135
+ this.stopSpinner();
136
+
137
+ // Block published apps from updating settings
138
+ if (this.app?.status && isPublished(this.app.status)) {
139
+ throw new Error(
140
+ "You can't edit settings for this integration. To edit your integration details on Zapier's public app directory, email partners@zapier.com."
141
+ );
142
+ }
143
+ }
144
+
145
+ appMeta.title = this.args.title?.trim();
146
+ if (!appMeta.title) {
147
+ appMeta.title = await this.prompt(
148
+ 'What is the title of your integration?',
149
+ {
150
+ required: true,
151
+ default: this.app?.title,
152
+ }
153
+ );
154
+ }
155
+
156
+ appMeta.description = this.flags.desc?.trim();
157
+ if (!appMeta.description) {
158
+ appMeta.description = await this.prompt(
159
+ `Please provide a sentence describing your app in ${MAX_DESCRIPTION_LENGTH} characters or less.`,
160
+ {
161
+ required: true,
162
+ charLimit: MAX_DESCRIPTION_LENGTH,
163
+ default: this.app?.description,
164
+ }
165
+ );
166
+ }
167
+
168
+ appMeta.homepage_url = this.flags.url;
169
+ if (!appMeta.homepage_url) {
170
+ appMeta.homepage_url = await this.prompt(
171
+ 'What is the homepage URL of your app? (optional)',
172
+ { default: this.app?.homepage_url }
173
+ );
174
+ }
175
+
176
+ appMeta.intention = this.flags.audience;
177
+ if (!appMeta.intention) {
178
+ appMeta.intention = await this.promptWithList(
179
+ 'Are you building a public or private integration?',
180
+ this.config.enumFieldChoices.intention,
181
+ { default: this.app?.intention }
182
+ );
183
+ }
184
+
185
+ appMeta.role = this.flags.role;
186
+ if (!appMeta.role) {
187
+ appMeta.role = await this.promptWithList(
188
+ "What is your relationship with the app you're integrating with Zapier?",
189
+ this._getRoleChoicesWithAppTitle(
190
+ appMeta.title,
191
+ this.config.enumFieldChoices.role
192
+ ),
193
+ { default: this.app?.role }
194
+ );
195
+ }
196
+
197
+ appMeta.app_category = this.flags.category;
198
+ if (!appMeta.app_category) {
199
+ appMeta.app_category = await this.promptWithList(
200
+ 'How would you categorize your app?',
201
+ this.config.enumFieldChoices.app_category,
202
+ { default: this.app?.app_category }
203
+ );
204
+ }
205
+
206
+ if (action === 'register') {
207
+ appMeta.subscription = this.flags.subscribe;
208
+ if (typeof this.flags.yes !== 'undefined') {
209
+ appMeta.subscription = true;
210
+ } else if (typeof appMeta.subscription === 'undefined') {
211
+ // boolean field, so using `typeof` === `undefined`
212
+ appMeta.subscription = await this.promptWithList(
213
+ 'Subscribe to Updates about your Integration',
214
+ [
215
+ { name: 'Yes', value: true },
216
+ { name: 'No', value: false },
217
+ ]
218
+ );
219
+ }
220
+ }
221
+
222
+ return { appMeta, action };
223
+ }
224
+
225
+ /**
226
+ *
227
+ * @param {string} title title of integration
228
+ * @param {array} choices retrieved role choices with `[app_title]` tokens
229
+ * @returns {array} array of choices with integration titles (instead of `[app_title]` tokens)
230
+ */
231
+ _getRoleChoicesWithAppTitle(title, choices) {
232
+ return choices.map((choice) => ({
233
+ value: choice.value,
234
+ name: choice.name.replace('[app_title]', title),
235
+ }));
24
236
  }
25
237
  }
26
238
 
@@ -32,15 +244,55 @@ RegisterCommand.args = [
32
244
  "Your integrations's public title. Asked interactively if not present.",
33
245
  },
34
246
  ];
35
- RegisterCommand.flags = buildFlags();
247
+
248
+ RegisterCommand.flags = buildFlags({
249
+ commandFlags: {
250
+ desc: flags.string({
251
+ char: 'D',
252
+ description: `A sentence describing your app in ${MAX_DESCRIPTION_LENGTH} characters or less, e.g. "Trello is a team collaboration tool to organize tasks and keep projects on track."`,
253
+ }),
254
+ url: flags.string({
255
+ char: 'u',
256
+ description: 'The homepage URL of your app, e.g., https://example.com.',
257
+ }),
258
+ audience: flags.string({
259
+ char: 'a',
260
+ description: 'Are you building a public or private integration?',
261
+ }),
262
+ role: flags.string({
263
+ char: 'r',
264
+ description:
265
+ "What is your relationship with the app you're integrating with Zapier?",
266
+ }),
267
+ category: flags.string({
268
+ char: 'c',
269
+ description:
270
+ "How would you categorize your app? Choose the most appropriate option for your app's core features.",
271
+ }),
272
+ subscribe: flags.boolean({
273
+ char: 's',
274
+ description:
275
+ 'Get tips and recommendations about this integration along with our monthly newsletter that details the performance of your integration and the latest Zapier news.',
276
+ allowNo: true,
277
+ }),
278
+ yes: flags.boolean({
279
+ char: 'y',
280
+ description:
281
+ 'Assume yes for all yes/no prompts. This flag will also update an existing integration (as opposed to registering a new one) if a .zapierapprc file is found.',
282
+ }),
283
+ },
284
+ });
36
285
  RegisterCommand.examples = [
37
286
  'zapier register',
38
287
  'zapier register "My Cool Integration"',
288
+ 'zapier register "My Cool Integration" --desc "My Cool Integration helps you integrate your apps with the apps that you need." --no-subscribe',
289
+ 'zapier register "My Cool Integration" --url "https://www.zapier.com" --audience private --role employee --category marketing-automation',
290
+ 'zapier register --subscribe',
39
291
  ];
40
- RegisterCommand.description = `Register a new integration in your account.
292
+ RegisterCommand.description = `Register a new integration in your account, or update the existing one if a \`${CURRENT_APP_FILE}\` file is found.
41
293
 
42
- After running this, you can run \`zapier push\` to build and upload your integration for use in the Zapier editor.
294
+ This command creates a new integration and links it in the \`./${CURRENT_APP_FILE}\` file. If \`${CURRENT_APP_FILE}\` already exists, it will ask you if you want to update the currently-linked integration, as opposed to creating a new one.
43
295
 
44
- This will change the \`./${CURRENT_APP_FILE}\` (which identifies this directory as holding code for a specific integration).`;
296
+ After registering a new integration, you can run \`zapier push\` to build and upload your integration for use in the Zapier editor. This will change \`${CURRENT_APP_FILE}\`, which identifies this directory as holding code for a specific integration.`;
45
297
 
46
298
  module.exports = RegisterCommand;
@@ -0,0 +1,45 @@
1
+ const { callAPI } = require('../../utils/api');
2
+
3
+ module.exports = async function (options) {
4
+ // We only need to run this for the register command
5
+ if (!options || !options.id || options.id !== 'register') {
6
+ return null;
7
+ }
8
+
9
+ const enumFieldChoices = {};
10
+ let formFields;
11
+
12
+ try {
13
+ formFields = await callAPI('/apps/fields-choices');
14
+ } catch (e) {
15
+ this.error(
16
+ `Unable to connect to Zapier API. Please check your connection and try again. ${e}`
17
+ );
18
+ }
19
+
20
+ for (const fieldName of ['intention', 'role', 'app_category']) {
21
+ enumFieldChoices[fieldName] = formFields[fieldName];
22
+ }
23
+
24
+ this.config.enumFieldChoices = enumFieldChoices;
25
+
26
+ // This enables us to see all available options when running `zapier register --help`
27
+ const cmd = options.config.findCommand('register');
28
+ if (cmd && cmd.flags) {
29
+ if (cmd.flags.audience) {
30
+ cmd.flags.audience.options = formFields.intention.map(
31
+ (audienceOption) => audienceOption.value
32
+ );
33
+ }
34
+ if (cmd.flags.role) {
35
+ cmd.flags.role.options = formFields.role.map(
36
+ (roleOption) => roleOption.value
37
+ );
38
+ }
39
+ if (cmd.flags.category) {
40
+ cmd.flags.category.options = formFields.app_category.map(
41
+ (categoryOption) => categoryOption.value
42
+ );
43
+ }
44
+ }
45
+ };
package/src/utils/api.js CHANGED
@@ -265,6 +265,12 @@ const getVersionInfo = () => {
265
265
  });
266
266
  };
267
267
 
268
+ // Intended to match logic of https://gitlab.com/zapier/team-developer-platform/dev-platform/-/blob/9fa28d8bacd04ebdad5937bd039c71aede4ede47/web/frontend/assets/app/entities/CliApp/CliApp.ts#L96
269
+ const isPublished = (appStatus) => {
270
+ const publishedStatuses = ['public', 'beta'];
271
+ return publishedStatuses.indexOf(appStatus) > -1;
272
+ };
273
+
268
274
  const listApps = async () => {
269
275
  let linkedApp;
270
276
  try {
@@ -405,6 +411,7 @@ module.exports = {
405
411
  getLinkedAppConfig,
406
412
  getWritableApp,
407
413
  getVersionInfo,
414
+ isPublished,
408
415
  listApps,
409
416
  listEndpoint,
410
417
  listEndpointMulti,
@@ -43,6 +43,8 @@ const {
43
43
  validateApp,
44
44
  } = require('./api');
45
45
 
46
+ const checkMissingAppInfo = require('./check-missing-app-info');
47
+
46
48
  const { runCommand, isWindows } = require('./misc');
47
49
 
48
50
  const debug = require('debug')('zapier:build');
@@ -167,6 +169,7 @@ const forceIncludeDumbPath = (appConfig, filePath) => {
167
169
  // include old async deasync versions so this runs seamlessly across node versions
168
170
  filePath.endsWith(path.join('bin', 'linux-x64-node-10', 'deasync.node')) ||
169
171
  filePath.endsWith(path.join('bin', 'linux-x64-node-12', 'deasync.node')) ||
172
+ filePath.endsWith(path.join('bin', 'linux-x64-node-14', 'deasync.node')) ||
170
173
  filePath.endsWith(
171
174
  // Special, for zapier-platform-legacy-scripting-runner
172
175
  path.join('bin', `linux-x64-node-${nodeMajorVersion}`, 'deasync.node')
@@ -497,6 +500,7 @@ const buildAndOrUpload = async (
497
500
  let app;
498
501
  if (upload) {
499
502
  app = await getWritableApp();
503
+ checkMissingAppInfo(app);
500
504
  }
501
505
 
502
506
  if (build) {
@@ -0,0 +1,26 @@
1
+ const { isPublished } = require('../utils/api');
2
+
3
+ module.exports = (app) => {
4
+ if (app.status && isPublished(app.status)) {
5
+ return false;
6
+ }
7
+ const requiredFields = [
8
+ { apiName: 'title' },
9
+ { apiName: 'description' },
10
+ { apiName: 'app_category', cliName: 'category' },
11
+ { apiName: 'intention', cliName: 'audience' },
12
+ { apiName: 'role' },
13
+ ];
14
+ const missingRequiredFields = requiredFields.filter(
15
+ (field) => app[field.apiName] == null
16
+ );
17
+ if (missingRequiredFields.length) {
18
+ throw new Error(
19
+ `Your integration is missing required info (${missingRequiredFields
20
+ .map((field) => field.cliName ?? field.apiName)
21
+ .join(', ')}). Please, run "zapier register" to add it.`
22
+ );
23
+ }
24
+
25
+ return false;
26
+ };
@@ -15,5 +15,5 @@ module.exports = [
15
15
  { nodeVersion: '12', npmVersion: '>=5.6.0' }, // 10.x
16
16
  { nodeVersion: '14', npmVersion: '>=5.6.0' }, // 11.x
17
17
  { nodeVersion: '14', npmVersion: '>=5.6.0' }, // 12.x
18
- // { nodeVersion: '16', npmVersion: '>=5.6.0' }, // 13.x
18
+ { nodeVersion: '16', npmVersion: '>=5.6.0' }, // 13.x
19
19
  ];