@zapier/zapier-sdk-cli 0.35.1 → 0.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +232 -0
- package/dist/cli.cjs +337 -168
- package/dist/cli.mjs +335 -167
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/package.json +7 -1
- package/dist/src/utils/cli-generator.js +46 -12
- package/dist/src/utils/parameter-resolver.d.ts +11 -0
- package/dist/src/utils/parameter-resolver.js +262 -157
- package/dist/src/utils/schema-formatter.d.ts +6 -1
- package/dist/src/utils/schema-formatter.js +45 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import inquirer from "inquirer";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { runWithTelemetryContext, } from "@zapier/zapier-sdk";
|
|
5
6
|
import { isPositional } from "@zapier/zapier-sdk";
|
|
@@ -61,9 +62,31 @@ function getLocalResolutionOrderForParams(paramNames, resolvers) {
|
|
|
61
62
|
// Schema Parameter Resolver
|
|
62
63
|
// ============================================================================
|
|
63
64
|
export class SchemaParameterResolver {
|
|
65
|
+
constructor() {
|
|
66
|
+
this.debug = false;
|
|
67
|
+
this.spinner = null;
|
|
68
|
+
}
|
|
69
|
+
debugLog(message) {
|
|
70
|
+
if (this.debug) {
|
|
71
|
+
this.stopSpinner();
|
|
72
|
+
console.log(chalk.gray(`[Zapier CLI] ${message}`));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
startSpinner() {
|
|
76
|
+
if (!this.debug && !this.spinner) {
|
|
77
|
+
this.spinner = ora({ text: "", spinner: "dots" }).start();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
stopSpinner() {
|
|
81
|
+
if (this.spinner) {
|
|
82
|
+
this.spinner.stop();
|
|
83
|
+
this.spinner = null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
64
86
|
async resolveParameters(schema, providedParams, sdk, functionName, options) {
|
|
65
87
|
return runWithTelemetryContext(async () => {
|
|
66
|
-
|
|
88
|
+
this.debug = options?.debug ?? false;
|
|
89
|
+
const interactiveMode = (options?.interactiveMode ?? true) && !!process.stdin.isTTY;
|
|
67
90
|
// 1. Try to parse with current parameters
|
|
68
91
|
const parseResult = schema.safeParse(providedParams);
|
|
69
92
|
// Get all schema parameters to check which ones have resolvers
|
|
@@ -74,47 +97,28 @@ export class SchemaParameterResolver {
|
|
|
74
97
|
const hasValue = this.getNestedValue(providedParams, param.path) !== undefined;
|
|
75
98
|
return !hasValue;
|
|
76
99
|
});
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
// - truly optional: only ask if user wants to be prompted
|
|
81
|
-
const functionallyRequired = missingResolvable.filter((param) => {
|
|
82
|
-
// Schema-required parameters are always functionally required
|
|
100
|
+
// Split missing resolvable params into required vs optional.
|
|
101
|
+
// "inputs" is treated as required in interactive mode.
|
|
102
|
+
const required = missingResolvable.filter((param) => {
|
|
83
103
|
if (param.isRequired)
|
|
84
104
|
return true;
|
|
85
|
-
|
|
86
|
-
// non-interactive mode (--json), where optional inputs should remain optional.
|
|
87
|
-
if (param.name === "inputs") {
|
|
105
|
+
if (param.name === "inputs")
|
|
88
106
|
return interactiveMode;
|
|
89
|
-
}
|
|
90
|
-
return false;
|
|
91
|
-
});
|
|
92
|
-
// Parameters that should always be prompted for directly, but can be skipped
|
|
93
|
-
const alwaysPrompt = missingResolvable.filter((param) => {
|
|
94
|
-
if (functionallyRequired.includes(param))
|
|
95
|
-
return false;
|
|
96
|
-
// connectionId should always be prompted for (since it's usually needed)
|
|
97
|
-
// but can be skipped with "Continue without connection"
|
|
98
|
-
if (param.name === "connectionId") {
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
107
|
return false;
|
|
102
108
|
});
|
|
103
|
-
const
|
|
104
|
-
!alwaysPrompt.includes(param));
|
|
109
|
+
const optional = missingResolvable.filter((param) => !required.includes(param));
|
|
105
110
|
if (parseResult.success &&
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
required.length === 0 &&
|
|
112
|
+
optional.length === 0) {
|
|
108
113
|
return parseResult.data;
|
|
109
114
|
}
|
|
110
|
-
if (
|
|
111
|
-
// No functionally required parameters missing, but check if we can parse
|
|
115
|
+
if (required.length === 0 && optional.length === 0) {
|
|
112
116
|
if (!parseResult.success) {
|
|
113
117
|
throw new ZapierCliValidationError(formatZodError(parseResult.error));
|
|
114
118
|
}
|
|
115
119
|
return parseResult.data;
|
|
116
120
|
}
|
|
117
|
-
// 2. Resolve
|
|
121
|
+
// 2. Resolve required parameters
|
|
118
122
|
const resolvedParams = { ...providedParams };
|
|
119
123
|
const context = {
|
|
120
124
|
sdk,
|
|
@@ -122,24 +126,16 @@ export class SchemaParameterResolver {
|
|
|
122
126
|
resolvedParams,
|
|
123
127
|
functionName,
|
|
124
128
|
};
|
|
125
|
-
// Get local resolvers for this function
|
|
126
129
|
const localResolvers = this.getLocalResolvers(sdk, functionName);
|
|
127
|
-
if (
|
|
128
|
-
const requiredParamNames =
|
|
130
|
+
if (required.length > 0) {
|
|
131
|
+
const requiredParamNames = required.map((p) => p.name);
|
|
129
132
|
const requiredResolutionOrder = getLocalResolutionOrderForParams(requiredParamNames, localResolvers);
|
|
130
133
|
// Find all parameters that need to be resolved (including dependencies)
|
|
131
|
-
// from the available resolvable parameters
|
|
132
134
|
const orderedRequiredParams = requiredResolutionOrder
|
|
133
135
|
.map((paramName) => {
|
|
134
|
-
|
|
135
|
-
let param = functionallyRequired.find((p) => p.name === paramName);
|
|
136
|
-
// If not found, try always prompt (for dependencies like connectionId)
|
|
136
|
+
let param = required.find((p) => p.name === paramName);
|
|
137
137
|
if (!param) {
|
|
138
|
-
param =
|
|
139
|
-
}
|
|
140
|
-
// If not found, try truly optional (for other dependencies)
|
|
141
|
-
if (!param) {
|
|
142
|
-
param = trulyOptional.find((p) => p.name === paramName);
|
|
138
|
+
param = optional.find((p) => p.name === paramName);
|
|
143
139
|
}
|
|
144
140
|
return param;
|
|
145
141
|
})
|
|
@@ -152,7 +148,6 @@ export class SchemaParameterResolver {
|
|
|
152
148
|
try {
|
|
153
149
|
const value = await this.resolveParameter(param, context, functionName);
|
|
154
150
|
this.setNestedValue(resolvedParams, param.path, value);
|
|
155
|
-
// Update context with newly resolved value
|
|
156
151
|
context.resolvedParams = resolvedParams;
|
|
157
152
|
}
|
|
158
153
|
catch (error) {
|
|
@@ -164,26 +159,25 @@ export class SchemaParameterResolver {
|
|
|
164
159
|
}
|
|
165
160
|
}
|
|
166
161
|
}
|
|
167
|
-
// Remove resolved dependencies from
|
|
162
|
+
// Remove resolved dependencies from optional to avoid double-prompting
|
|
168
163
|
const resolvedParamNames = new Set(orderedRequiredParams.map((p) => p.name));
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
const orderedAlwaysPromptParams = alwaysPromptResolutionOrder
|
|
179
|
-
.map((paramName) => alwaysPrompt.find((p) => p.name === paramName))
|
|
164
|
+
optional.splice(0, optional.length, ...optional.filter((p) => !resolvedParamNames.has(p.name)));
|
|
165
|
+
}
|
|
166
|
+
// 3. Resolve optional parameters individually (each is skippable).
|
|
167
|
+
// Skipped in non-interactive mode.
|
|
168
|
+
if (interactiveMode && optional.length > 0) {
|
|
169
|
+
const optionalParamNames = optional.map((p) => p.name);
|
|
170
|
+
const optionalResolutionOrder = getLocalResolutionOrderForParams(optionalParamNames, localResolvers);
|
|
171
|
+
const orderedOptionalParams = optionalResolutionOrder
|
|
172
|
+
.map((paramName) => optional.find((p) => p.name === paramName))
|
|
180
173
|
.filter((param) => param !== undefined);
|
|
181
|
-
for (const param of
|
|
174
|
+
for (const param of orderedOptionalParams) {
|
|
182
175
|
try {
|
|
183
|
-
const value = await this.resolveParameter(param, context, functionName);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
176
|
+
const value = await this.resolveParameter(param, context, functionName, { isOptional: true });
|
|
177
|
+
if (value !== undefined) {
|
|
178
|
+
this.setNestedValue(resolvedParams, param.path, value);
|
|
179
|
+
context.resolvedParams = resolvedParams;
|
|
180
|
+
}
|
|
187
181
|
}
|
|
188
182
|
catch (error) {
|
|
189
183
|
if (this.isUserCancellation(error)) {
|
|
@@ -194,42 +188,7 @@ export class SchemaParameterResolver {
|
|
|
194
188
|
}
|
|
195
189
|
}
|
|
196
190
|
}
|
|
197
|
-
// 4.
|
|
198
|
-
if (interactiveMode && trulyOptional.length > 0) {
|
|
199
|
-
const optionalNames = trulyOptional.map((p) => p.name).join(", ");
|
|
200
|
-
const shouldResolveOptional = await inquirer.prompt([
|
|
201
|
-
{
|
|
202
|
-
type: "confirm",
|
|
203
|
-
name: "resolveOptional",
|
|
204
|
-
message: `Would you like to be prompted for optional parameters (${optionalNames})?`,
|
|
205
|
-
default: false,
|
|
206
|
-
},
|
|
207
|
-
]);
|
|
208
|
-
if (shouldResolveOptional.resolveOptional) {
|
|
209
|
-
// Resolve optional parameters using their resolvers
|
|
210
|
-
const optionalParamNames = trulyOptional.map((p) => p.name);
|
|
211
|
-
const optionalResolutionOrder = getLocalResolutionOrderForParams(optionalParamNames, localResolvers);
|
|
212
|
-
const orderedOptionalParams = optionalResolutionOrder
|
|
213
|
-
.map((paramName) => trulyOptional.find((p) => p.name === paramName))
|
|
214
|
-
.filter((param) => param !== undefined);
|
|
215
|
-
for (const param of orderedOptionalParams) {
|
|
216
|
-
try {
|
|
217
|
-
const value = await this.resolveParameter(param, context, functionName);
|
|
218
|
-
this.setNestedValue(resolvedParams, param.path, value);
|
|
219
|
-
// Update context with newly resolved value
|
|
220
|
-
context.resolvedParams = resolvedParams;
|
|
221
|
-
}
|
|
222
|
-
catch (error) {
|
|
223
|
-
if (this.isUserCancellation(error)) {
|
|
224
|
-
console.log(chalk.yellow("\n\nOperation cancelled by user"));
|
|
225
|
-
throw new ZapierCliUserCancellationError();
|
|
226
|
-
}
|
|
227
|
-
throw error;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
// 5. Validate final parameters
|
|
191
|
+
// 4. Validate final parameters
|
|
233
192
|
const finalResult = schema.safeParse(resolvedParams);
|
|
234
193
|
if (!finalResult.success) {
|
|
235
194
|
throw new ZapierCliValidationError(`Parameter validation failed: ${formatZodError(finalResult.error)}`);
|
|
@@ -321,48 +280,97 @@ export class SchemaParameterResolver {
|
|
|
321
280
|
throw new ZapierCliMissingParametersError(missingParams);
|
|
322
281
|
}
|
|
323
282
|
}
|
|
324
|
-
async resolveParameter(param, context, functionName) {
|
|
283
|
+
async resolveParameter(param, context, functionName, options) {
|
|
325
284
|
const resolver = this.getResolver(param.name, context.sdk, functionName);
|
|
326
285
|
if (!resolver) {
|
|
327
286
|
throw new Error(`No resolver found for parameter: ${param.name}`);
|
|
328
287
|
}
|
|
329
|
-
|
|
288
|
+
return this.resolveWithResolver(resolver, param, context, {
|
|
289
|
+
isOptional: options?.isOptional,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
async resolveWithResolver(resolver, param, context, options = {}) {
|
|
293
|
+
const { arrayIndex, isOptional } = options;
|
|
294
|
+
const inArrayContext = arrayIndex != null;
|
|
295
|
+
const promptLabel = inArrayContext
|
|
296
|
+
? `${param.name}[${arrayIndex}]`
|
|
297
|
+
: param.name;
|
|
298
|
+
const promptName = inArrayContext ? "value" : param.name;
|
|
299
|
+
this.debugLog(`Resolving ${promptLabel}${isOptional ? " (optional)" : ""}`);
|
|
330
300
|
if (resolver.type === "static") {
|
|
331
301
|
const staticResolver = resolver;
|
|
332
302
|
const promptConfig = {
|
|
333
303
|
type: staticResolver.inputType === "password" ? "password" : "input",
|
|
334
|
-
name:
|
|
335
|
-
message: `Enter ${
|
|
304
|
+
name: promptName,
|
|
305
|
+
message: `Enter ${promptLabel}${isOptional ? " (optional)" : ""}:`,
|
|
336
306
|
...(staticResolver.placeholder && {
|
|
337
307
|
default: staticResolver.placeholder,
|
|
338
308
|
}),
|
|
339
309
|
};
|
|
310
|
+
this.stopSpinner();
|
|
340
311
|
const answers = await inquirer.prompt([promptConfig]);
|
|
341
|
-
|
|
312
|
+
const value = answers[promptName];
|
|
313
|
+
if (isOptional && (value === undefined || value === "")) {
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
return value;
|
|
342
317
|
}
|
|
343
318
|
else if (resolver.type === "dynamic") {
|
|
344
319
|
const dynamicResolver = resolver;
|
|
320
|
+
this.startSpinner();
|
|
345
321
|
const autoResolution = await this.tryAutoResolve(dynamicResolver, context);
|
|
346
322
|
if (autoResolution != null) {
|
|
323
|
+
this.stopSpinner();
|
|
347
324
|
return autoResolution.resolvedValue;
|
|
348
325
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
326
|
+
this.debugLog(`Fetching options for ${promptLabel}`);
|
|
327
|
+
const fetchResult = await dynamicResolver.fetch(context.sdk, context.resolvedParams);
|
|
328
|
+
const items = Array.isArray(fetchResult)
|
|
329
|
+
? fetchResult
|
|
330
|
+
: (fetchResult?.data ?? []);
|
|
331
|
+
const promptConfig = dynamicResolver.prompt(items, context.resolvedParams);
|
|
332
|
+
promptConfig.name = promptName;
|
|
333
|
+
// Inject a skip option for optional parameters
|
|
334
|
+
this.stopSpinner();
|
|
335
|
+
if (isOptional && promptConfig.choices) {
|
|
336
|
+
const SKIP_SENTINEL = Symbol("SKIP");
|
|
337
|
+
promptConfig.choices = [
|
|
338
|
+
{ name: chalk.dim("(Skip)"), value: SKIP_SENTINEL },
|
|
339
|
+
...promptConfig.choices,
|
|
340
|
+
];
|
|
341
|
+
const answers = await inquirer.prompt([promptConfig]);
|
|
342
|
+
const value = answers[promptName];
|
|
343
|
+
if (value === SKIP_SENTINEL) {
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
346
|
+
return value;
|
|
352
347
|
}
|
|
353
|
-
const items = await dynamicResolver.fetch(context.sdk, context.resolvedParams);
|
|
354
|
-
// Let the resolver's prompt handle empty lists (e.g., connectionId can show "skip connection")
|
|
355
|
-
const safeItems = items || [];
|
|
356
|
-
const promptConfig = dynamicResolver.prompt(safeItems, context.resolvedParams);
|
|
357
348
|
const answers = await inquirer.prompt([promptConfig]);
|
|
358
|
-
return answers[
|
|
349
|
+
return answers[promptName];
|
|
359
350
|
}
|
|
360
351
|
else if (resolver.type === "fields") {
|
|
361
|
-
|
|
352
|
+
if (isOptional && !inArrayContext) {
|
|
353
|
+
this.stopSpinner();
|
|
354
|
+
const { confirm } = await inquirer.prompt([
|
|
355
|
+
{
|
|
356
|
+
type: "confirm",
|
|
357
|
+
name: "confirm",
|
|
358
|
+
message: `Add ${promptLabel}?`,
|
|
359
|
+
default: false,
|
|
360
|
+
},
|
|
361
|
+
]);
|
|
362
|
+
if (!confirm) {
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return await this.resolveFieldsRecursively(resolver, context, param, { inArrayContext });
|
|
367
|
+
}
|
|
368
|
+
else if (resolver.type === "array") {
|
|
369
|
+
return await this.resolveArrayRecursively(resolver, context, param);
|
|
362
370
|
}
|
|
363
|
-
throw new Error(`Unknown resolver type for ${
|
|
371
|
+
throw new Error(`Unknown resolver type for ${promptLabel}`);
|
|
364
372
|
}
|
|
365
|
-
async resolveFieldsRecursively(resolver, context, param) {
|
|
373
|
+
async resolveFieldsRecursively(resolver, context, param, options = {}) {
|
|
366
374
|
const inputs = {};
|
|
367
375
|
let processedFieldKeys = new Set();
|
|
368
376
|
let iteration = 0;
|
|
@@ -377,8 +385,10 @@ export class SchemaParameterResolver {
|
|
|
377
385
|
inputs,
|
|
378
386
|
},
|
|
379
387
|
};
|
|
380
|
-
|
|
388
|
+
this.debugLog(`Fetching input fields for ${param.name}${iteration > 1 ? ` (iteration ${iteration})` : ""}`);
|
|
389
|
+
this.startSpinner();
|
|
381
390
|
const rootFieldItems = await resolver.fetch(updatedContext.sdk, updatedContext.resolvedParams);
|
|
391
|
+
this.stopSpinner();
|
|
382
392
|
if (!rootFieldItems || rootFieldItems.length === 0) {
|
|
383
393
|
if (iteration === 1) {
|
|
384
394
|
console.log(chalk.yellow(`No input fields required for this action.`));
|
|
@@ -386,7 +396,7 @@ export class SchemaParameterResolver {
|
|
|
386
396
|
break;
|
|
387
397
|
}
|
|
388
398
|
// Process fields recursively, maintaining fieldset structure
|
|
389
|
-
const fieldStats = await this.processFieldItems(rootFieldItems, inputs, processedFieldKeys, [], iteration, updatedContext);
|
|
399
|
+
const fieldStats = await this.processFieldItems(rootFieldItems, inputs, processedFieldKeys, [], iteration, updatedContext, { inArrayContext: options.inArrayContext });
|
|
390
400
|
// If no new fields were processed, we're done
|
|
391
401
|
if (fieldStats.newRequired === 0 && fieldStats.newOptional === 0) {
|
|
392
402
|
break;
|
|
@@ -399,13 +409,56 @@ export class SchemaParameterResolver {
|
|
|
399
409
|
if (iteration >= maxIterations) {
|
|
400
410
|
console.log(chalk.yellow(`\n⚠️ Maximum field resolution iterations reached. Some dynamic fields may not have been discovered.`));
|
|
401
411
|
}
|
|
412
|
+
// Apply transform if provided
|
|
413
|
+
if (resolver.transform) {
|
|
414
|
+
return resolver.transform(inputs);
|
|
415
|
+
}
|
|
402
416
|
return inputs;
|
|
403
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* Resolves an array parameter by repeatedly prompting for items until user says no
|
|
420
|
+
*/
|
|
421
|
+
async resolveArrayRecursively(resolver, context, param) {
|
|
422
|
+
const items = [];
|
|
423
|
+
const minItems = resolver.minItems ?? 0;
|
|
424
|
+
const maxItems = resolver.maxItems ?? Infinity;
|
|
425
|
+
while (items.length < maxItems) {
|
|
426
|
+
const currentIndex = items.length;
|
|
427
|
+
// Skip confirmation if we haven't hit minItems yet - just collect the item
|
|
428
|
+
if (currentIndex >= minItems) {
|
|
429
|
+
this.stopSpinner();
|
|
430
|
+
const confirmAnswer = await inquirer.prompt([
|
|
431
|
+
{
|
|
432
|
+
type: "confirm",
|
|
433
|
+
name: "addItem",
|
|
434
|
+
message: `Add ${param.name}[${currentIndex}]?`,
|
|
435
|
+
default: false,
|
|
436
|
+
},
|
|
437
|
+
]);
|
|
438
|
+
if (!confirmAnswer.addItem) {
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Fetch the inner resolver for this item
|
|
443
|
+
const innerResolver = await resolver.fetch(context.sdk, context.resolvedParams);
|
|
444
|
+
const itemValue = await this.resolveWithResolver(innerResolver, param, context, { arrayIndex: currentIndex });
|
|
445
|
+
items.push(itemValue);
|
|
446
|
+
// Update context with current array for subsequent iterations
|
|
447
|
+
context.resolvedParams = {
|
|
448
|
+
...context.resolvedParams,
|
|
449
|
+
[param.name]: items,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
if (items.length >= maxItems) {
|
|
453
|
+
console.log(chalk.gray(`Maximum of ${maxItems} items reached.`));
|
|
454
|
+
}
|
|
455
|
+
return items;
|
|
456
|
+
}
|
|
404
457
|
/**
|
|
405
458
|
* Recursively processes fieldsets and their fields, maintaining natural structure
|
|
406
459
|
* and creating nested inputs as needed (e.g., fieldset "foo" becomes inputs.foo = [{}])
|
|
407
460
|
*/
|
|
408
|
-
async processFieldItems(items, targetInputs, processedFieldKeys, fieldsetPath = [], iteration = 1, context) {
|
|
461
|
+
async processFieldItems(items, targetInputs, processedFieldKeys, fieldsetPath = [], iteration = 1, context, options = {}) {
|
|
409
462
|
let newRequiredCount = 0;
|
|
410
463
|
let newOptionalCount = 0;
|
|
411
464
|
let optionalSkipped = false;
|
|
@@ -423,7 +476,7 @@ export class SchemaParameterResolver {
|
|
|
423
476
|
// Process fields within this fieldset recursively
|
|
424
477
|
const fieldsetTarget = targetInputs[typedItem.key][0];
|
|
425
478
|
const nestedPath = [...fieldsetPath, fieldsetTitle];
|
|
426
|
-
const nestedStats = await this.processFieldItems(typedItem.fields, fieldsetTarget, processedFieldKeys, nestedPath, iteration, context);
|
|
479
|
+
const nestedStats = await this.processFieldItems(typedItem.fields, fieldsetTarget, processedFieldKeys, nestedPath, iteration, context, options);
|
|
427
480
|
newRequiredCount += nestedStats.newRequired;
|
|
428
481
|
newOptionalCount += nestedStats.newOptional;
|
|
429
482
|
if (nestedStats.optionalSkipped) {
|
|
@@ -439,11 +492,18 @@ export class SchemaParameterResolver {
|
|
|
439
492
|
if (isRequired) {
|
|
440
493
|
// Process required field immediately
|
|
441
494
|
newRequiredCount++;
|
|
442
|
-
if (
|
|
443
|
-
|
|
444
|
-
|
|
495
|
+
if (typedItem.resolver && context) {
|
|
496
|
+
const param = {
|
|
497
|
+
name: typedItem.key,
|
|
498
|
+
path: [typedItem.key],
|
|
499
|
+
schema: z.unknown(),
|
|
500
|
+
isRequired: true,
|
|
501
|
+
};
|
|
502
|
+
targetInputs[typedItem.key] = await this.resolveWithResolver(typedItem.resolver, param, context, { isOptional: false });
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
await this.promptForField(typedItem, targetInputs, context);
|
|
445
506
|
}
|
|
446
|
-
await this.promptForField(typedItem, targetInputs, context);
|
|
447
507
|
processedFieldKeys.add(typedItem.key);
|
|
448
508
|
}
|
|
449
509
|
else {
|
|
@@ -464,39 +524,49 @@ export class SchemaParameterResolver {
|
|
|
464
524
|
});
|
|
465
525
|
if (optionalFields.length > 0) {
|
|
466
526
|
const pathContext = fieldsetPath.length > 0 ? ` in ${fieldsetPath.join(" > ")}` : "";
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
message: `Would you like to configure ${iteration === 1 ? "" : "these additional "}optional fields${pathContext}?`,
|
|
474
|
-
default: false,
|
|
475
|
-
},
|
|
476
|
-
]);
|
|
477
|
-
if (shouldConfigureOptional.configure) {
|
|
478
|
-
console.log(chalk.cyan(`\nOptional fields${pathContext}:`));
|
|
479
|
-
for (const field of optionalFields) {
|
|
480
|
-
await this.promptForField(field, targetInputs, context);
|
|
481
|
-
const typedField = field;
|
|
482
|
-
processedFieldKeys.add(typedField.key);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
else {
|
|
486
|
-
optionalSkipped = true;
|
|
487
|
-
// Mark these fields as processed even if skipped to avoid re-asking
|
|
488
|
-
optionalFields.forEach((field) => {
|
|
489
|
-
const typedField = field;
|
|
490
|
-
processedFieldKeys.add(typedField.key);
|
|
491
|
-
});
|
|
527
|
+
// In array context, prompt for all fields directly without confirmation
|
|
528
|
+
if (options.inArrayContext) {
|
|
529
|
+
for (const field of optionalFields) {
|
|
530
|
+
await this.promptForField(field, targetInputs, context);
|
|
531
|
+
const typedField = field;
|
|
532
|
+
processedFieldKeys.add(typedField.key);
|
|
492
533
|
}
|
|
493
534
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
535
|
+
else {
|
|
536
|
+
console.log(chalk.gray(`\nThere are ${optionalFields.length} ${iteration === 1 ? "" : "additional "}optional field(s) available${pathContext}.`));
|
|
537
|
+
try {
|
|
538
|
+
const shouldConfigureOptional = await inquirer.prompt([
|
|
539
|
+
{
|
|
540
|
+
type: "confirm",
|
|
541
|
+
name: "configure",
|
|
542
|
+
message: `Would you like to configure ${iteration === 1 ? "" : "these additional "}optional fields${pathContext}?`,
|
|
543
|
+
default: false,
|
|
544
|
+
},
|
|
545
|
+
]);
|
|
546
|
+
if (shouldConfigureOptional.configure) {
|
|
547
|
+
console.log(chalk.cyan(`\nOptional fields${pathContext}:`));
|
|
548
|
+
for (const field of optionalFields) {
|
|
549
|
+
await this.promptForField(field, targetInputs, context);
|
|
550
|
+
const typedField = field;
|
|
551
|
+
processedFieldKeys.add(typedField.key);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
optionalSkipped = true;
|
|
556
|
+
// Mark these fields as processed even if skipped to avoid re-asking
|
|
557
|
+
optionalFields.forEach((field) => {
|
|
558
|
+
const typedField = field;
|
|
559
|
+
processedFieldKeys.add(typedField.key);
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
if (this.isUserCancellation(error)) {
|
|
565
|
+
console.log(chalk.yellow("\n\nOperation cancelled by user"));
|
|
566
|
+
throw new ZapierCliUserCancellationError();
|
|
567
|
+
}
|
|
568
|
+
throw error;
|
|
498
569
|
}
|
|
499
|
-
throw error;
|
|
500
570
|
}
|
|
501
571
|
}
|
|
502
572
|
}
|
|
@@ -533,9 +603,10 @@ export class SchemaParameterResolver {
|
|
|
533
603
|
isRequired: fieldObj.is_required || false,
|
|
534
604
|
defaultValue: fieldObj.default_value ?? fieldObj.default,
|
|
535
605
|
valueType,
|
|
536
|
-
hasDropdown: fieldObj.format === "SELECT",
|
|
606
|
+
hasDropdown: fieldObj.format === "SELECT" || Boolean(fieldObj.choices),
|
|
537
607
|
isMultiSelect: Boolean(valueType === "array" ||
|
|
538
608
|
(fieldObj.items && fieldObj.items.type !== undefined)),
|
|
609
|
+
inlineChoices: fieldObj.choices,
|
|
539
610
|
};
|
|
540
611
|
}
|
|
541
612
|
/**
|
|
@@ -543,9 +614,10 @@ export class SchemaParameterResolver {
|
|
|
543
614
|
*/
|
|
544
615
|
async fetchChoices(fieldMeta, inputs, context, cursor) {
|
|
545
616
|
try {
|
|
546
|
-
|
|
547
|
-
? `
|
|
548
|
-
: `
|
|
617
|
+
this.debugLog(cursor
|
|
618
|
+
? `Fetching more choices for ${fieldMeta.title}`
|
|
619
|
+
: `Fetching choices for ${fieldMeta.title}`);
|
|
620
|
+
this.startSpinner();
|
|
549
621
|
const page = await context.sdk.listInputFieldChoices({
|
|
550
622
|
appKey: context.resolvedParams.appKey,
|
|
551
623
|
actionKey: context.resolvedParams.actionKey,
|
|
@@ -555,12 +627,13 @@ export class SchemaParameterResolver {
|
|
|
555
627
|
inputs,
|
|
556
628
|
...(cursor && { cursor }),
|
|
557
629
|
});
|
|
630
|
+
this.stopSpinner();
|
|
558
631
|
const choices = page.data.map((choice) => ({
|
|
559
632
|
label: choice.label || choice.key || String(choice.value),
|
|
560
633
|
value: choice.value ?? choice.key,
|
|
561
634
|
}));
|
|
562
635
|
if (choices.length === 0 && !cursor) {
|
|
563
|
-
console.log(chalk.yellow(`
|
|
636
|
+
console.log(chalk.yellow(`No choices available for ${fieldMeta.title}`));
|
|
564
637
|
}
|
|
565
638
|
return {
|
|
566
639
|
choices,
|
|
@@ -568,7 +641,8 @@ export class SchemaParameterResolver {
|
|
|
568
641
|
};
|
|
569
642
|
}
|
|
570
643
|
catch (error) {
|
|
571
|
-
|
|
644
|
+
this.stopSpinner();
|
|
645
|
+
console.warn(chalk.yellow(`Failed to fetch choices for ${fieldMeta.title}:`), error);
|
|
572
646
|
return { choices: [] };
|
|
573
647
|
}
|
|
574
648
|
}
|
|
@@ -576,6 +650,7 @@ export class SchemaParameterResolver {
|
|
|
576
650
|
* Prompt user with choices (handles both single and multi-select with pagination)
|
|
577
651
|
*/
|
|
578
652
|
async promptWithChoices({ fieldMeta, choices: initialChoices, nextCursor: initialCursor, inputs, context, }) {
|
|
653
|
+
this.stopSpinner();
|
|
579
654
|
const choices = [...initialChoices];
|
|
580
655
|
let nextCursor = initialCursor;
|
|
581
656
|
const LOAD_MORE_SENTINEL = Symbol("LOAD_MORE");
|
|
@@ -647,6 +722,31 @@ export class SchemaParameterResolver {
|
|
|
647
722
|
? Boolean(fieldMeta.defaultValue)
|
|
648
723
|
: undefined;
|
|
649
724
|
}
|
|
725
|
+
else if (fieldMeta.valueType === "array") {
|
|
726
|
+
promptConfig.type = "input";
|
|
727
|
+
promptConfig.default = fieldMeta.defaultValue;
|
|
728
|
+
promptConfig.message = `${fieldMeta.title}${fieldMeta.isRequired ? " (required)" : " (optional)"} (JSON array or comma-separated):`;
|
|
729
|
+
promptConfig.validate = (input) => {
|
|
730
|
+
if (fieldMeta.isRequired && !input) {
|
|
731
|
+
return "This field is required";
|
|
732
|
+
}
|
|
733
|
+
return true;
|
|
734
|
+
};
|
|
735
|
+
promptConfig.filter = (input) => {
|
|
736
|
+
if (!input)
|
|
737
|
+
return input;
|
|
738
|
+
const trimmed = input.trim();
|
|
739
|
+
if (trimmed.startsWith("[")) {
|
|
740
|
+
try {
|
|
741
|
+
return JSON.parse(trimmed);
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
// Fall through to comma-separated
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return trimmed.split(",").map((s) => s.trim());
|
|
748
|
+
};
|
|
749
|
+
}
|
|
650
750
|
else {
|
|
651
751
|
promptConfig.type = "input";
|
|
652
752
|
promptConfig.default = fieldMeta.defaultValue;
|
|
@@ -695,10 +795,15 @@ export class SchemaParameterResolver {
|
|
|
695
795
|
}
|
|
696
796
|
async promptForField(field, inputs, context) {
|
|
697
797
|
const fieldMeta = this.extractFieldMetadata(field);
|
|
698
|
-
//
|
|
798
|
+
// Get choices - either inline or fetched from API
|
|
699
799
|
let choices = [];
|
|
700
800
|
let nextCursor;
|
|
701
|
-
if (fieldMeta.
|
|
801
|
+
if (fieldMeta.inlineChoices) {
|
|
802
|
+
// Use inline choices directly
|
|
803
|
+
choices = fieldMeta.inlineChoices;
|
|
804
|
+
}
|
|
805
|
+
else if (fieldMeta.hasDropdown && context) {
|
|
806
|
+
// Fetch choices from API
|
|
702
807
|
const result = await this.fetchChoices(fieldMeta, inputs, context);
|
|
703
808
|
choices = result.choices;
|
|
704
809
|
nextCursor = result.nextCursor;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
|
+
import type { OutputFormatter, ZapierSdk } from "@zapier/zapier-sdk";
|
|
2
3
|
export declare function formatJsonOutput(data: unknown): void;
|
|
3
4
|
export declare function formatItemsFromSchema(functionInfo: {
|
|
4
5
|
inputSchema: z.ZodType;
|
|
5
6
|
outputSchema?: z.ZodType;
|
|
6
|
-
}, items: unknown[], startingNumber?: number
|
|
7
|
+
}, items: unknown[], startingNumber?: number, options?: {
|
|
8
|
+
formatter?: OutputFormatter;
|
|
9
|
+
sdk?: ZapierSdk;
|
|
10
|
+
params?: Record<string, unknown>;
|
|
11
|
+
}): Promise<void>;
|