@zapier/zapier-sdk-cli 0.9.0 → 0.11.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.
@@ -2,11 +2,6 @@ import inquirer from "inquirer";
2
2
  import chalk from "chalk";
3
3
  import { z } from "zod";
4
4
  import type { ZapierSdk } from "@zapier/zapier-sdk";
5
- import {
6
- getResolver,
7
- hasResolver,
8
- getResolutionOrderForParams,
9
- } from "@zapier/zapier-sdk";
10
5
 
11
6
  // ============================================================================
12
7
  // Types
@@ -24,6 +19,66 @@ interface ResolverContext {
24
19
  sdk: ZapierSdk;
25
20
  currentParams: Record<string, unknown>;
26
21
  resolvedParams: Record<string, unknown>;
22
+ functionName?: string;
23
+ }
24
+
25
+ // ============================================================================
26
+ // Local Resolution Helper Functions
27
+ // ============================================================================
28
+
29
+ /**
30
+ * Resolve dependency chain for a parameter using local resolvers
31
+ * Returns parameters in the order they need to be resolved
32
+ */
33
+ function getLocalResolutionOrder(
34
+ paramName: string,
35
+ resolvers: Record<string, any>,
36
+ resolved: Set<string> = new Set(),
37
+ ): string[] {
38
+ const resolver = resolvers[paramName];
39
+ if (!resolver || resolver.type === "static") {
40
+ return [paramName];
41
+ }
42
+
43
+ const order: string[] = [];
44
+
45
+ if ("depends" in resolver && resolver.depends) {
46
+ for (const dependency of resolver.depends) {
47
+ if (!resolved.has(dependency)) {
48
+ order.push(...getLocalResolutionOrder(dependency, resolvers, resolved));
49
+ resolved.add(dependency);
50
+ }
51
+ }
52
+ }
53
+
54
+ if (!resolved.has(paramName)) {
55
+ order.push(paramName);
56
+ resolved.add(paramName);
57
+ }
58
+
59
+ return order;
60
+ }
61
+
62
+ /**
63
+ * Get resolution order for multiple parameters using local resolvers
64
+ */
65
+ function getLocalResolutionOrderForParams(
66
+ paramNames: string[],
67
+ resolvers: Record<string, any>,
68
+ ): string[] {
69
+ const resolved = new Set<string>();
70
+ const order: string[] = [];
71
+
72
+ for (const paramName of paramNames) {
73
+ const paramOrder = getLocalResolutionOrder(paramName, resolvers, resolved);
74
+ for (const param of paramOrder) {
75
+ if (!order.includes(param)) {
76
+ order.push(param);
77
+ }
78
+ }
79
+ }
80
+
81
+ return order;
27
82
  }
28
83
 
29
84
  // ============================================================================
@@ -35,6 +90,7 @@ export class SchemaParameterResolver {
35
90
  schema: z.ZodSchema,
36
91
  providedParams: unknown,
37
92
  sdk: ZapierSdk,
93
+ functionName?: string,
38
94
  ): Promise<unknown> {
39
95
  // 1. Try to parse with current parameters
40
96
  const parseResult = schema.safeParse(providedParams);
@@ -42,7 +98,7 @@ export class SchemaParameterResolver {
42
98
  // Get all schema parameters to check which ones have resolvers
43
99
  const allParams = this.extractParametersFromSchema(schema);
44
100
  const resolvableParams = allParams.filter((param) =>
45
- hasResolver(param.name),
101
+ this.hasResolver(param.name, sdk, functionName),
46
102
  );
47
103
 
48
104
  // Get all missing parameters that have resolvers
@@ -108,12 +164,18 @@ export class SchemaParameterResolver {
108
164
  sdk,
109
165
  currentParams: providedParams as Record<string, unknown>,
110
166
  resolvedParams,
167
+ functionName,
111
168
  };
112
169
 
170
+ // Get local resolvers for this function
171
+ const localResolvers = this.getLocalResolvers(sdk, functionName);
172
+
113
173
  if (functionallyRequired.length > 0) {
114
174
  const requiredParamNames = functionallyRequired.map((p) => p.name);
115
- const requiredResolutionOrder =
116
- getResolutionOrderForParams(requiredParamNames);
175
+ const requiredResolutionOrder = getLocalResolutionOrderForParams(
176
+ requiredParamNames,
177
+ localResolvers,
178
+ );
117
179
 
118
180
  // Find all parameters that need to be resolved (including dependencies)
119
181
  // from the available resolvable parameters
@@ -135,7 +197,11 @@ export class SchemaParameterResolver {
135
197
 
136
198
  for (const param of orderedRequiredParams) {
137
199
  try {
138
- const value = await this.resolveParameter(param, context);
200
+ const value = await this.resolveParameter(
201
+ param,
202
+ context,
203
+ functionName,
204
+ );
139
205
  this.setNestedValue(resolvedParams, param.path, value);
140
206
 
141
207
  // Update context with newly resolved value
@@ -168,8 +234,10 @@ export class SchemaParameterResolver {
168
234
  // 3. Resolve parameters that should always be prompted for (but can be skipped)
169
235
  if (alwaysPrompt.length > 0) {
170
236
  const alwaysPromptNames = alwaysPrompt.map((p) => p.name);
171
- const alwaysPromptResolutionOrder =
172
- getResolutionOrderForParams(alwaysPromptNames);
237
+ const alwaysPromptResolutionOrder = getLocalResolutionOrderForParams(
238
+ alwaysPromptNames,
239
+ localResolvers,
240
+ );
173
241
 
174
242
  const orderedAlwaysPromptParams = alwaysPromptResolutionOrder
175
243
  .map((paramName) => alwaysPrompt.find((p) => p.name === paramName))
@@ -177,7 +245,11 @@ export class SchemaParameterResolver {
177
245
 
178
246
  for (const param of orderedAlwaysPromptParams) {
179
247
  try {
180
- const value = await this.resolveParameter(param, context);
248
+ const value = await this.resolveParameter(
249
+ param,
250
+ context,
251
+ functionName,
252
+ );
181
253
  this.setNestedValue(resolvedParams, param.path, value);
182
254
 
183
255
  // Update context with newly resolved value
@@ -207,8 +279,10 @@ export class SchemaParameterResolver {
207
279
  if (shouldResolveOptional.resolveOptional) {
208
280
  // Resolve optional parameters using their resolvers
209
281
  const optionalParamNames = trulyOptional.map((p) => p.name);
210
- const optionalResolutionOrder =
211
- getResolutionOrderForParams(optionalParamNames);
282
+ const optionalResolutionOrder = getLocalResolutionOrderForParams(
283
+ optionalParamNames,
284
+ localResolvers,
285
+ );
212
286
 
213
287
  const orderedOptionalParams = optionalResolutionOrder
214
288
  .map((paramName) => trulyOptional.find((p) => p.name === paramName))
@@ -216,7 +290,11 @@ export class SchemaParameterResolver {
216
290
 
217
291
  for (const param of orderedOptionalParams) {
218
292
  try {
219
- const value = await this.resolveParameter(param, context);
293
+ const value = await this.resolveParameter(
294
+ param,
295
+ context,
296
+ functionName,
297
+ );
220
298
  this.setNestedValue(resolvedParams, param.path, value);
221
299
 
222
300
  // Update context with newly resolved value
@@ -306,8 +384,9 @@ export class SchemaParameterResolver {
306
384
  private async resolveParameter(
307
385
  param: ResolvableParameter,
308
386
  context: ResolverContext,
387
+ functionName?: string,
309
388
  ): Promise<unknown> {
310
- const resolver = getResolver(param.name);
389
+ const resolver = this.getResolver(param.name, context.sdk, functionName);
311
390
  if (!resolver) {
312
391
  throw new Error(`No resolver found for parameter: ${param.name}`);
313
392
  }
@@ -380,7 +459,7 @@ export class SchemaParameterResolver {
380
459
  const inputs: Record<string, unknown> = {};
381
460
  let processedFieldKeys = new Set<string>();
382
461
  let iteration = 0;
383
- const maxIterations = 5; // Prevent infinite loops
462
+ const maxIterations = 10; // Prevent infinite loops
384
463
 
385
464
  while (iteration < maxIterations) {
386
465
  iteration++;
@@ -400,12 +479,12 @@ export class SchemaParameterResolver {
400
479
  ),
401
480
  );
402
481
 
403
- const fields = await typedResolver.fetch(
482
+ const rootFieldItems = await typedResolver.fetch(
404
483
  updatedContext.sdk,
405
484
  updatedContext.resolvedParams,
406
485
  );
407
486
 
408
- if (!fields || fields.length === 0) {
487
+ if (!rootFieldItems || rootFieldItems.length === 0) {
409
488
  if (iteration === 1) {
410
489
  console.log(
411
490
  chalk.yellow(`No input fields required for this action.`),
@@ -414,59 +493,180 @@ export class SchemaParameterResolver {
414
493
  break;
415
494
  }
416
495
 
417
- // Find new fields that we haven't processed yet
418
- const newFields = fields.filter(
419
- (field: unknown) =>
420
- !(
421
- (field as { key: string }).key &&
422
- processedFieldKeys.has((field as { key: string }).key)
423
- ),
496
+ // Process fields recursively, maintaining fieldset structure
497
+ const fieldStats = await this.processFieldItems(
498
+ rootFieldItems,
499
+ inputs,
500
+ processedFieldKeys,
501
+ [],
502
+ iteration,
424
503
  );
425
504
 
426
- if (newFields.length === 0) {
427
- // No new fields, we're done
505
+ // If no new fields were processed, we're done
506
+ if (fieldStats.newRequired === 0 && fieldStats.newOptional === 0) {
428
507
  break;
429
508
  }
430
509
 
431
- // Separate new required and optional fields
432
- const newRequiredFields = newFields.filter(
433
- (field: unknown) => (field as { required: boolean }).required,
434
- );
435
- const newOptionalFields = newFields.filter(
436
- (field: unknown) => !(field as { required: boolean }).required,
510
+ // If we only processed optional fields and skipped them, no need to re-fetch
511
+ if (fieldStats.newRequired === 0 && fieldStats.optionalSkipped) {
512
+ break;
513
+ }
514
+ }
515
+
516
+ if (iteration >= maxIterations) {
517
+ console.log(
518
+ chalk.yellow(
519
+ `\n⚠️ Maximum field resolution iterations reached. Some dynamic fields may not have been discovered.`,
520
+ ),
437
521
  );
522
+ }
438
523
 
439
- // Prompt for new required fields
440
- if (newRequiredFields.length > 0) {
524
+ return inputs;
525
+ }
526
+
527
+ /**
528
+ * Recursively processes fieldsets and their fields, maintaining natural structure
529
+ * and creating nested inputs as needed (e.g., fieldset "foo" becomes inputs.foo = [{}])
530
+ */
531
+ private async processFieldItems(
532
+ items: unknown[],
533
+ targetInputs: Record<string, unknown>,
534
+ processedFieldKeys: Set<string>,
535
+ fieldsetPath: string[] = [],
536
+ iteration: number = 1,
537
+ ): Promise<{
538
+ newRequired: number;
539
+ newOptional: number;
540
+ optionalSkipped: boolean;
541
+ }> {
542
+ let newRequiredCount = 0;
543
+ let newOptionalCount = 0;
544
+ let optionalSkipped = false;
545
+
546
+ for (const item of items) {
547
+ const typedItem = item as {
548
+ type?: string;
549
+ key?: string;
550
+ title?: string;
551
+ fields?: unknown[];
552
+ is_required?: boolean;
553
+ };
554
+
555
+ if (typedItem.type === "fieldset" && typedItem.fields && typedItem.key) {
556
+ // Show fieldset context to user
557
+ const fieldsetTitle = typedItem.title || typedItem.key;
558
+ const pathDisplay =
559
+ fieldsetPath.length > 0 ? ` (in ${fieldsetPath.join(" > ")})` : "";
441
560
  console.log(
442
- chalk.blue(
443
- `\n📝 Please provide values for the following ${iteration === 1 ? "" : "additional "}input fields:`,
561
+ chalk.cyan(
562
+ `\n📁 Processing fieldset: ${fieldsetTitle}${pathDisplay}`,
444
563
  ),
445
564
  );
446
- for (const field of newRequiredFields) {
447
- await this.promptForField(field, inputs);
448
- processedFieldKeys.add((field as { key: string }).key);
565
+
566
+ // Create fieldset array in target inputs if it doesn't exist
567
+ if (!targetInputs[typedItem.key]) {
568
+ targetInputs[typedItem.key] = [{}];
569
+ }
570
+
571
+ // Process fields within this fieldset recursively
572
+ const fieldsetTarget = (
573
+ targetInputs[typedItem.key] as Record<string, unknown>[]
574
+ )[0];
575
+ const nestedPath = [...fieldsetPath, fieldsetTitle];
576
+
577
+ const nestedStats = await this.processFieldItems(
578
+ typedItem.fields,
579
+ fieldsetTarget,
580
+ processedFieldKeys,
581
+ nestedPath,
582
+ iteration,
583
+ );
584
+
585
+ newRequiredCount += nestedStats.newRequired;
586
+ newOptionalCount += nestedStats.newOptional;
587
+ if (nestedStats.optionalSkipped) {
588
+ optionalSkipped = true;
589
+ }
590
+ } else if (typedItem.type === "input_field" && typedItem.key) {
591
+ // Skip if already processed
592
+ if (processedFieldKeys.has(typedItem.key)) {
593
+ continue;
594
+ }
595
+
596
+ const isRequired = typedItem.is_required || false;
597
+
598
+ if (isRequired) {
599
+ // Process required field immediately
600
+ newRequiredCount++;
601
+ if (newRequiredCount === 1 && fieldsetPath.length === 0) {
602
+ // Only show this message once at root level
603
+ console.log(
604
+ chalk.blue(
605
+ `\n📝 Please provide values for the following ${iteration === 1 ? "" : "additional "}input fields:`,
606
+ ),
607
+ );
608
+ }
609
+
610
+ await this.promptForField(typedItem, targetInputs);
611
+ processedFieldKeys.add(typedItem.key);
612
+ } else {
613
+ // Collect optional fields for batch processing
614
+ newOptionalCount++;
449
615
  }
450
616
  }
617
+ // Skip info fields - they're for display only
618
+ }
619
+
620
+ // Handle optional fields after processing all required fields
621
+ if (newOptionalCount > 0) {
622
+ const optionalFields = items.filter((item: unknown) => {
623
+ const typedItem = item as {
624
+ type?: string;
625
+ key?: string;
626
+ is_required?: boolean;
627
+ };
628
+ return (
629
+ typedItem.type === "input_field" &&
630
+ typedItem.key &&
631
+ !typedItem.is_required &&
632
+ !processedFieldKeys.has(typedItem.key)
633
+ );
634
+ });
451
635
 
452
- // Ask about optional fields
453
- let shouldConfigureOptional = { configure: false };
454
- if (newOptionalFields.length > 0) {
636
+ if (optionalFields.length > 0) {
637
+ const pathContext =
638
+ fieldsetPath.length > 0 ? ` in ${fieldsetPath.join(" > ")}` : "";
455
639
  console.log(
456
640
  chalk.gray(
457
- `\nThere are ${newOptionalFields.length} ${iteration === 1 ? "" : "additional "}optional field(s) available.`,
641
+ `\nThere are ${optionalFields.length} ${iteration === 1 ? "" : "additional "}optional field(s) available${pathContext}.`,
458
642
  ),
459
643
  );
460
644
 
461
645
  try {
462
- shouldConfigureOptional = await inquirer.prompt([
646
+ const shouldConfigureOptional = await inquirer.prompt([
463
647
  {
464
648
  type: "confirm",
465
649
  name: "configure",
466
- message: `Would you like to configure ${iteration === 1 ? "" : "these additional "}optional fields?`,
650
+ message: `Would you like to configure ${iteration === 1 ? "" : "these additional "}optional fields${pathContext}?`,
467
651
  default: false,
468
652
  },
469
653
  ]);
654
+
655
+ if (shouldConfigureOptional.configure) {
656
+ console.log(chalk.cyan(`\nOptional fields${pathContext}:`));
657
+ for (const field of optionalFields) {
658
+ await this.promptForField(field, targetInputs);
659
+ const typedField = field as { key: string };
660
+ processedFieldKeys.add(typedField.key);
661
+ }
662
+ } else {
663
+ optionalSkipped = true;
664
+ // Mark these fields as processed even if skipped to avoid re-asking
665
+ optionalFields.forEach((field: unknown) => {
666
+ const typedField = field as { key: string };
667
+ processedFieldKeys.add(typedField.key);
668
+ });
669
+ }
470
670
  } catch (error) {
471
671
  if (this.isUserCancellation(error)) {
472
672
  console.log(chalk.yellow("\n\nOperation cancelled by user"));
@@ -474,39 +674,14 @@ export class SchemaParameterResolver {
474
674
  }
475
675
  throw error;
476
676
  }
477
-
478
- if (shouldConfigureOptional.configure) {
479
- console.log(chalk.cyan(`\nOptional fields:`));
480
- for (const field of newOptionalFields) {
481
- await this.promptForField(field, inputs);
482
- processedFieldKeys.add((field as { key: string }).key);
483
- }
484
- } else {
485
- // Mark these fields as processed even if skipped to avoid re-asking
486
- newOptionalFields.forEach((field: unknown) =>
487
- processedFieldKeys.add((field as { key: string }).key),
488
- );
489
- }
490
- }
491
-
492
- // If we only processed optional fields and skipped them, no need to re-fetch
493
- if (
494
- newRequiredFields.length === 0 &&
495
- (!newOptionalFields.length || !shouldConfigureOptional.configure)
496
- ) {
497
- break;
498
677
  }
499
678
  }
500
679
 
501
- if (iteration >= maxIterations) {
502
- console.log(
503
- chalk.yellow(
504
- `\n⚠️ Maximum field resolution iterations reached. Some dynamic fields may not have been discovered.`,
505
- ),
506
- );
507
- }
508
-
509
- return inputs;
680
+ return {
681
+ newRequired: newRequiredCount,
682
+ newOptional: newOptionalCount,
683
+ optionalSkipped,
684
+ };
510
685
  }
511
686
 
512
687
  private getNestedValue(obj: unknown, path: string[]): unknown {
@@ -536,7 +711,7 @@ export class SchemaParameterResolver {
536
711
  type?: string;
537
712
  key: string;
538
713
  label?: string;
539
- required?: boolean;
714
+ is_required?: boolean;
540
715
  helpText?: string;
541
716
  default?: unknown;
542
717
  choices?: Array<{ label?: string; value: unknown }>;
@@ -545,7 +720,7 @@ export class SchemaParameterResolver {
545
720
  const fieldPrompt: Record<string, unknown> = {
546
721
  type: fieldObj.type === "boolean" ? "confirm" : "input",
547
722
  name: fieldObj.key,
548
- message: `${fieldObj.label || fieldObj.key}${fieldObj.required ? " (required)" : " (optional)"}:`,
723
+ message: `${fieldObj.label || fieldObj.key}${fieldObj.is_required ? " (required)" : " (optional)"}:`,
549
724
  };
550
725
 
551
726
  if (fieldObj.helpText) {
@@ -574,7 +749,7 @@ export class SchemaParameterResolver {
574
749
 
575
750
  if (answer[fieldObj.key] !== undefined && answer[fieldObj.key] !== "") {
576
751
  inputs[fieldObj.key] = answer[fieldObj.key];
577
- } else if (fieldObj.required) {
752
+ } else if (fieldObj.is_required) {
578
753
  throw new Error(`Required field ${fieldObj.key} cannot be empty`);
579
754
  }
580
755
  } catch (error) {
@@ -598,4 +773,59 @@ export class SchemaParameterResolver {
598
773
  errorObj?.isTTYError === true
599
774
  );
600
775
  }
776
+
777
+ private hasResolver(
778
+ paramName: string,
779
+ sdk: ZapierSdk,
780
+ functionName?: string,
781
+ ): boolean {
782
+ // Check plugin-specific resolvers first
783
+ if (functionName && typeof sdk.getRegistry === "function") {
784
+ const registry = sdk.getRegistry();
785
+ const functionInfo = registry.functions.find(
786
+ (f) => f.name === functionName,
787
+ );
788
+ if (functionInfo && functionInfo.resolvers?.[paramName]) {
789
+ return true;
790
+ }
791
+ }
792
+
793
+ // No global registry fallback
794
+ return false;
795
+ }
796
+
797
+ private getResolver(
798
+ paramName: string,
799
+ sdk: ZapierSdk,
800
+ functionName?: string,
801
+ ): any {
802
+ // Check plugin-specific resolvers first
803
+ if (functionName && typeof sdk.getRegistry === "function") {
804
+ const registry = sdk.getRegistry();
805
+ const functionInfo = registry.functions.find(
806
+ (f) => f.name === functionName,
807
+ );
808
+ if (functionInfo && functionInfo.resolvers?.[paramName]) {
809
+ return functionInfo.resolvers[paramName];
810
+ }
811
+ }
812
+
813
+ // No global registry fallback
814
+ return null;
815
+ }
816
+
817
+ private getLocalResolvers(
818
+ sdk: ZapierSdk,
819
+ functionName?: string,
820
+ ): Record<string, any> {
821
+ if (!functionName || typeof sdk.getRegistry !== "function") {
822
+ return {};
823
+ }
824
+
825
+ const registry = sdk.getRegistry();
826
+ const functionInfo = registry.functions.find(
827
+ (f) => f.name === functionName,
828
+ );
829
+ return functionInfo?.resolvers || {};
830
+ }
601
831
  }