@workos/oagen-emitters 0.14.4 → 0.15.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.
Files changed (43) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +12 -0
  3. package/dist/index.d.mts.map +1 -1
  4. package/dist/index.mjs +1 -1
  5. package/dist/{plugin-BGVaMGqe.mjs → plugin-CO4RFgAW.mjs} +959 -251
  6. package/dist/plugin-CO4RFgAW.mjs.map +1 -0
  7. package/dist/plugin.mjs +1 -1
  8. package/package.json +7 -7
  9. package/renovate.json +1 -61
  10. package/src/go/client.ts +1 -1
  11. package/src/go/enums.ts +77 -0
  12. package/src/kotlin/enums.ts +11 -4
  13. package/src/node/client.ts +119 -2
  14. package/src/node/discriminated-models.ts +8 -0
  15. package/src/node/field-plan.ts +64 -8
  16. package/src/node/index.ts +59 -3
  17. package/src/node/models.ts +73 -30
  18. package/src/node/naming.ts +14 -1
  19. package/src/node/node-overrides.ts +4 -37
  20. package/src/node/options.ts +29 -1
  21. package/src/node/resources.ts +533 -83
  22. package/src/node/tests.ts +108 -7
  23. package/src/php/fixtures.ts +4 -1
  24. package/src/php/models.ts +3 -1
  25. package/src/php/resources.ts +40 -11
  26. package/src/php/tests.ts +22 -12
  27. package/src/python/client.ts +0 -8
  28. package/src/python/enums.ts +41 -15
  29. package/src/python/fixtures.ts +23 -7
  30. package/src/python/models.ts +26 -5
  31. package/src/python/resources.ts +71 -3
  32. package/src/python/tests.ts +70 -12
  33. package/src/python/wrappers.ts +25 -4
  34. package/src/ruby/client.ts +0 -1
  35. package/src/rust/resources.ts +10 -7
  36. package/src/shared/non-spec-services.ts +0 -5
  37. package/test/go/enums.test.ts +24 -0
  38. package/test/node/resources.test.ts +11 -1
  39. package/test/node/tests.test.ts +3 -3
  40. package/test/php/client.test.ts +0 -1
  41. package/test/php/resources.test.ts +50 -0
  42. package/test/rust/resources.test.ts +9 -0
  43. package/dist/plugin-BGVaMGqe.mjs.map +0 -1
@@ -1,5 +1,7 @@
1
1
  // @oagen-ignore: Operation.async — all TypeScript SDK methods are async by nature
2
2
 
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
3
5
  import type {
4
6
  Service,
5
7
  Operation,
@@ -12,7 +14,12 @@ import type {
12
14
  import { planOperation, toPascalCase, toCamelCase } from '@workos/oagen';
13
15
  import type { OperationPlan } from '@workos/oagen';
14
16
  import { mapTypeRef, isInlineEnum } from './type-map.js';
15
- import { liveSurfaceHasFunction, liveSurfaceHasFile, liveSurfaceHasAutogenFile } from './live-surface.js';
17
+ import {
18
+ liveSurfaceHasFunction,
19
+ liveSurfaceHasFile,
20
+ liveSurfaceHasAutogenFile,
21
+ liveSurfaceInterfacePath,
22
+ } from './live-surface.js';
16
23
 
17
24
  /**
18
25
  * Render the request-body argument for an HTTP call.
@@ -77,7 +84,7 @@ import {
77
84
  import { generateWrapperMethods, collectWrapperResponseModels } from './wrappers.js';
78
85
  import { buildNodePathExpression } from './path-expression.js';
79
86
  import { resolveWrapperParams } from '../shared/wrapper-utils.js';
80
- import { isNodeOwnedService } from './options.js';
87
+ import { isNodeOwnedService, nodeOptions } from './options.js';
81
88
 
82
89
  /**
83
90
  * Check whether the baseline (hand-written) class has a constructor compatible
@@ -156,18 +163,151 @@ type BaselineMethod = {
156
163
  returnType?: string;
157
164
  };
158
165
 
166
+ type OptionsObjectParam = {
167
+ name: 'options';
168
+ type: string;
169
+ optional: boolean;
170
+ generated: boolean;
171
+ };
172
+
173
+ type GeneratedOptionInterfaceExport = {
174
+ stem: string;
175
+ typeName: string;
176
+ };
177
+
178
+ function recordGeneratedOptionInterface(ctx: EmitterContext, serviceDir: string, stem: string, typeName: string): void {
179
+ const key = '_nodeGeneratedOptionInterfaceExports';
180
+ const registry = ((ctx as any)[key] ??= new Map<string, GeneratedOptionInterfaceExport[]>()) as Map<
181
+ string,
182
+ GeneratedOptionInterfaceExport[]
183
+ >;
184
+ const exports = registry.get(serviceDir) ?? [];
185
+ if (!exports.some((entry) => entry.stem === stem && entry.typeName === typeName)) {
186
+ exports.push({ stem, typeName });
187
+ }
188
+ registry.set(serviceDir, exports);
189
+ }
190
+
191
+ function existingInterfaceBarrelExports(ctx: EmitterContext, serviceDir: string, stem: string): boolean {
192
+ const root = ctx.outputDir ?? ctx.targetDir;
193
+ if (!root) return false;
194
+ const barrelPath = path.join(root, 'src', serviceDir, 'interfaces', 'index.ts');
195
+ let content: string;
196
+ try {
197
+ content = fs.readFileSync(barrelPath, 'utf8');
198
+ } catch {
199
+ return false;
200
+ }
201
+ const escapedStem = stem.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
202
+ return new RegExp(`export\\s+(?:type\\s+)?(?:\\*|\\{[^}]+\\})\\s+from\\s+['"]\\./${escapedStem}['"]`).test(content);
203
+ }
204
+
205
+ function operationOverrideFor(ctx: EmitterContext, op: Operation) {
206
+ return nodeOptions(ctx).operationOverrides?.[`${op.httpMethod.toUpperCase()} ${op.path}`];
207
+ }
208
+
159
209
  function baselineMethodFor(service: Service, method: string, ctx: EmitterContext): BaselineMethod | undefined {
160
210
  const serviceClass = resolveResourceClassName(service, ctx);
161
211
  return ctx.apiSurface?.classes?.[serviceClass]?.methods?.[method]?.[0] as BaselineMethod | undefined;
162
212
  }
163
213
 
164
- function optionsObjectParam(method: BaselineMethod | undefined): { name: string; type: string } | undefined {
214
+ function ignoredResourceMethodNames(ctx: EmitterContext, resourcePath: string): Set<string> {
215
+ const root = ctx.outputDir ?? ctx.targetDir;
216
+ if (!root) return new Set();
217
+
218
+ let content: string;
219
+ try {
220
+ content = fs.readFileSync(path.join(root, resourcePath), 'utf8');
221
+ } catch {
222
+ return new Set();
223
+ }
224
+
225
+ const methods = new Set<string>();
226
+ for (const block of content.matchAll(/@oagen-ignore-start[\s\S]*?@oagen-ignore-end/g)) {
227
+ for (const line of block[0].split('\n')) {
228
+ const match = line.match(/^\s{2}(?:(?:public|private|protected)\s+)?(?:async\s+)?([A-Za-z_$][\w$]*)\s*\(/);
229
+ if (match) methods.add(match[1]);
230
+ }
231
+ }
232
+ return methods;
233
+ }
234
+
235
+ function optionsObjectParam(method: BaselineMethod | undefined): OptionsObjectParam | undefined {
165
236
  if (!method || method.params.length !== 1) return undefined;
166
237
  const [param] = method.params;
167
238
  if (param.name !== 'options') return undefined;
168
239
  if (param.passingStyle && param.passingStyle !== 'options_object') return undefined;
169
240
  if (!param.type || /^(Record|object|any|unknown)\b/.test(param.type)) return undefined;
170
- return { name: param.name, type: param.type };
241
+ return { name: 'options', type: param.type, optional: param.optional === true, generated: false };
242
+ }
243
+
244
+ function methodOptionsName(method: string, resolvedServiceName: string): string {
245
+ if (method === 'list') return `${toPascalCase(resolvedServiceName)}ListOptions`;
246
+ return `${toPascalCase(method)}Options`;
247
+ }
248
+
249
+ function hiddenParamsFor(resolvedOp?: ResolvedOperation): Set<string> {
250
+ return new Set<string>([...Object.keys(getOpDefaults(resolvedOp)), ...getOpInferFromClient(resolvedOp)]);
251
+ }
252
+
253
+ function visibleQueryParamsForOptions(op: Operation, plan: OperationPlan, resolvedOp?: ResolvedOperation) {
254
+ const hidden = hiddenParamsFor(resolvedOp);
255
+ return op.queryParams.filter((param) => {
256
+ if (hidden.has(param.name)) return false;
257
+ if (plan.isPaginated && PAGINATION_PARAM_NAMES.has(param.name)) return false;
258
+ return true;
259
+ });
260
+ }
261
+
262
+ function optionsObjectShouldBeOptional(op: Operation, plan: OperationPlan, resolvedOp?: ResolvedOperation): boolean {
263
+ if (plan.hasBody) return false;
264
+ if (op.pathParams.length > 0) return false;
265
+ return visibleQueryParamsForOptions(op, plan, resolvedOp).every((param) => !param.required);
266
+ }
267
+
268
+ function operationHasOptionsInput(op: Operation, plan: OperationPlan, resolvedOp?: ResolvedOperation): boolean {
269
+ return (
270
+ op.pathParams.length > 0 ||
271
+ plan.hasBody ||
272
+ plan.isPaginated ||
273
+ visibleQueryParamsForOptions(op, plan, resolvedOp).length > 0
274
+ );
275
+ }
276
+
277
+ function optionsObjectInfo(
278
+ service: Service,
279
+ method: string,
280
+ op: Operation,
281
+ plan: OperationPlan,
282
+ ctx: EmitterContext,
283
+ baselineMethod: BaselineMethod | undefined,
284
+ resolvedOp?: ResolvedOperation,
285
+ ): OptionsObjectParam | undefined {
286
+ const baseline = optionsObjectParam(baselineMethod);
287
+ if (baseline) return baseline;
288
+
289
+ const overrideType = operationOverrideFor(ctx, op)?.optionsType;
290
+ if (overrideType) {
291
+ return {
292
+ name: 'options',
293
+ type: overrideType,
294
+ optional: optionsObjectShouldBeOptional(op, plan, resolvedOp),
295
+ generated: baselineTypeSourceFile(ctx, overrideType) === undefined,
296
+ };
297
+ }
298
+
299
+ if (!operationHasOptionsInput(op, plan, resolvedOp)) return undefined;
300
+
301
+ return {
302
+ name: 'options',
303
+ type: methodOptionsName(method, resolveResourceClassName(service, ctx)),
304
+ optional: optionsObjectShouldBeOptional(op, plan, resolvedOp),
305
+ generated: true,
306
+ };
307
+ }
308
+
309
+ function renderOptionsParam(param: OptionsObjectParam): string {
310
+ return `options${param.optional ? '?' : ''}: ${param.type}`;
171
311
  }
172
312
 
173
313
  function autoPaginatableItemType(returnType: string | undefined): string | undefined {
@@ -317,21 +457,14 @@ function deduplicateMethodNames(
317
457
  }
318
458
 
319
459
  /**
320
- * Emit one interface file per paginated list operation that has extension
321
- * query params. Placing the options interface under `interfaces/` lets the
322
- * per-service barrel pick it up via `export * from './interfaces'`, which
323
- * is what the root `src/index.ts` re-exports. When the interface was
324
- * declared inline in the resource file, it was unreachable from the barrel
325
- * and callers couldn't import the type by name from the package root.
460
+ * Emit one interface/type file per generated options-object operation.
461
+ * Placing the options type under `interfaces/` lets the per-service barrel
462
+ * pick it up via `export * from './interfaces'`.
326
463
  */
327
- function generatePaginatedOptionsInterfaces(
328
- service: Service,
329
- ctx: EmitterContext,
330
- specEnumNames: Set<string>,
331
- ): GeneratedFile[] {
464
+ function generateOptionsInterfaces(service: Service, ctx: EmitterContext, specEnumNames: Set<string>): GeneratedFile[] {
332
465
  const files: GeneratedFile[] = [];
333
- const resolvedName = resolveResourceClassName(service, ctx);
334
466
  const serviceDir = resolveResourceDir(service, ctx);
467
+ const resolvedLookup = buildResolvedLookup(ctx);
335
468
 
336
469
  const plans = service.operations.map((op) => ({
337
470
  op,
@@ -340,28 +473,125 @@ function generatePaginatedOptionsInterfaces(
340
473
  }));
341
474
 
342
475
  for (const { op, plan, method } of plans) {
343
- if (!plan.isPaginated) continue;
344
- const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
345
- if (extraParams.length === 0) continue;
476
+ const resolvedOp = lookupResolved(op, resolvedLookup);
477
+ const baselineMethod = baselineMethodFor(service, method, ctx);
478
+ const optionInfo = optionsObjectInfo(service, method, op, plan, ctx, baselineMethod, resolvedOp);
479
+ if (!optionInfo?.generated) continue;
480
+ if (baselineTypeSourceFile(ctx, optionInfo.type)) continue;
481
+
482
+ const optionsName = optionInfo.type;
483
+ const optionFileStem = `${fileName(optionsName)}.interface`;
484
+ const filePath = `src/${serviceDir}/interfaces/${optionFileStem}.ts`;
485
+ if (!liveSurfaceHasFile(filePath) || existingInterfaceBarrelExports(ctx, serviceDir, optionFileStem)) {
486
+ recordGeneratedOptionInterface(ctx, serviceDir, optionFileStem, optionsName);
487
+ }
346
488
 
347
- const optionsName = paginatedOptionsName(method, resolvedName);
348
- const filePath = `src/${serviceDir}/interfaces/${fileName(optionsName)}.interface.ts`;
489
+ const optEnums = new Set<string>();
490
+ const optModels = new Set<string>();
491
+ for (const param of [...op.pathParams, ...visibleQueryParamsForOptions(op, plan, resolvedOp)]) {
492
+ collectParamTypeRefs(param.type, optEnums, optModels);
493
+ }
494
+ const bodyInfo = extractRequestBodyType(op, ctx);
495
+ if (bodyInfo?.kind === 'model') {
496
+ const bodyModel = ctx.spec.models.find((m) => m.name === bodyInfo.name);
497
+ if (bodyModel) {
498
+ for (const field of bodyModel.fields) {
499
+ collectParamTypeRefs(field.type, optEnums, optModels);
500
+ }
501
+ }
502
+ } else if (bodyInfo?.kind === 'union') {
503
+ for (const name of bodyInfo.modelNames) {
504
+ optModels.add(name);
505
+ }
506
+ }
349
507
 
350
508
  const lines: string[] = [];
351
- lines.push("import type { PaginationOptions } from '../../common/interfaces/pagination-options.interface';");
509
+ if (plan.isPaginated) {
510
+ lines.push("import type { PaginationOptions } from '../../common/interfaces/pagination-options.interface';");
511
+ }
512
+ const { modelToService, resolveDir } = createServiceDirResolver(ctx.spec.models, ctx.spec.services, ctx);
513
+ if (optEnums.size > 0) {
514
+ const enumToService = assignEnumsToServices(ctx.spec.enums, ctx.spec.services, ctx.spec.models, ctx);
515
+ for (const name of optEnums) {
516
+ if (!specEnumNames.has(name)) continue;
517
+ if (isInlineEnum(name)) continue;
518
+ const enumDir = resolveDir(enumToService.get(name));
519
+ const relPath =
520
+ enumDir === serviceDir
521
+ ? `./${fileName(name)}.interface`
522
+ : `../../${enumDir}/interfaces/${fileName(name)}.interface`;
523
+ lines.push(`import type { ${name} } from '${relPath}';`);
524
+ }
525
+ }
526
+ for (const name of optModels) {
527
+ const modelDir = resolveDir(modelToService.get(name));
528
+ const relPath =
529
+ modelDir === serviceDir
530
+ ? `./${fileName(name)}.interface`
531
+ : `../../${modelDir}/interfaces/${fileName(name)}.interface`;
532
+ lines.push(`import type { ${resolveInterfaceName(name, ctx)} } from '${relPath}';`);
533
+ }
352
534
  lines.push('');
353
- lines.push(`export interface ${optionsName} extends PaginationOptions {`);
354
- for (const param of extraParams) {
355
- const opt = !param.required ? '?' : '';
356
- if (param.description || param.deprecated) {
535
+
536
+ const headerParts: string[] = [];
537
+ const pushField = (name: string, required: boolean, type: string, description?: string, deprecated?: boolean) => {
538
+ const opt = !required ? '?' : '';
539
+ if (description || deprecated) {
357
540
  const parts: string[] = [];
358
- if (param.description) parts.push(param.description);
359
- if (param.deprecated) parts.push('@deprecated');
360
- lines.push(...docComment(parts.join('\n'), 2));
541
+ if (description) parts.push(description);
542
+ if (deprecated) parts.push('@deprecated');
543
+ headerParts.push(...docComment(parts.join('\n'), 2));
544
+ }
545
+ headerParts.push(` ${name}${opt}: ${type};`);
546
+ };
547
+
548
+ for (const param of op.pathParams) {
549
+ pushField(
550
+ fieldName(param.name),
551
+ true,
552
+ mapParamType(param.type, specEnumNames),
553
+ param.description,
554
+ param.deprecated,
555
+ );
556
+ }
557
+ for (const param of visibleQueryParamsForOptions(op, plan, resolvedOp)) {
558
+ pushField(
559
+ fieldName(param.name),
560
+ param.required,
561
+ mapParamType(param.type, specEnumNames),
562
+ param.description,
563
+ param.deprecated,
564
+ );
565
+ }
566
+
567
+ if (bodyInfo?.kind === 'model') {
568
+ const bodyModel = ctx.spec.models.find((m) => m.name === bodyInfo.name);
569
+ if (bodyModel) {
570
+ for (const field of bodyModel.fields) {
571
+ pushField(
572
+ fieldName(field.name),
573
+ field.required,
574
+ mapParamType(field.type, specEnumNames),
575
+ field.description,
576
+ field.deprecated,
577
+ );
578
+ }
579
+ }
580
+ lines.push(`export interface ${optionsName}${plan.isPaginated ? ' extends PaginationOptions' : ''} {`);
581
+ lines.push(...headerParts);
582
+ lines.push('}');
583
+ } else if (bodyInfo?.kind === 'union') {
584
+ const baseType = headerParts.length > 0 ? `{\n${headerParts.join('\n')}\n}` : '{}';
585
+ lines.push(`export type ${optionsName} = ${baseType} & (${bodyInfo.typeStr});`);
586
+ } else {
587
+ if (plan.isPaginated && headerParts.length === 0) {
588
+ lines.push(`export type ${optionsName} = PaginationOptions;`);
589
+ } else {
590
+ lines.push(`export interface ${optionsName}${plan.isPaginated ? ' extends PaginationOptions' : ''} {`);
591
+ lines.push(...headerParts);
592
+ lines.push('}');
361
593
  }
362
- lines.push(` ${fieldName(param.name)}${opt}: ${mapParamType(param.type, specEnumNames)};`);
363
594
  }
364
- lines.push('}');
365
595
 
366
596
  files.push({
367
597
  path: filePath,
@@ -435,7 +665,7 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
435
665
  const isOwnedService = isNodeOwnedService(ctx, service.name, resolveResourceClassName(service, ctx));
436
666
  if (!isOwnedService && isServiceCoveredByExisting(service, ctx) && !hasMethodsAbsentFromBaseline(service, ctx))
437
667
  continue;
438
- files.push(...generatePaginatedOptionsInterfaces(service, ctx, topLevelEnumNames));
668
+ files.push(...generateOptionsInterfaces(service, ctx, topLevelEnumNames));
439
669
  }
440
670
 
441
671
  return files;
@@ -504,8 +734,12 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
504
734
  // Done after dedup + sort so the rendered methods keep their stable names.
505
735
  // Imports below filter through `plans`, so they automatically narrow to
506
736
  // the kept operations only — no orphan imports.
737
+ const ignoredMethodNames = ignoredResourceMethodNames(ctx, resourcePath);
507
738
  const baselineMethodNames = new Set(Object.keys(ctx.apiSurface?.classes?.[serviceClass]?.methods ?? {}));
508
739
  const planCountBeforeFilter = plans.length;
740
+ if (ignoredMethodNames.size > 0) {
741
+ plans = plans.filter((p) => !ignoredMethodNames.has(p.method));
742
+ }
509
743
  if (!isNodeOwnedService(ctx, service.name, serviceClass) && baselineMethodNames.size > 0) {
510
744
  plans = plans.filter((p) => !baselineMethodNames.has(p.method));
511
745
  }
@@ -520,38 +754,66 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
520
754
  }
521
755
 
522
756
  const hasPaginated = plans.some((p) => p.plan.isPaginated);
523
- const needsPaginationOptionsImport = plans.some(
524
- (p) =>
757
+ const resolvedLookup = buildResolvedLookup(ctx);
758
+ const needsPaginationOptionsImport = plans.some((p) => {
759
+ const baseline = baselineMethodFor(service, p.method, ctx);
760
+ const optionInfo = optionsObjectInfo(
761
+ service,
762
+ p.method,
763
+ p.op,
764
+ p.plan,
765
+ ctx,
766
+ baseline,
767
+ lookupResolved(p.op, resolvedLookup),
768
+ );
769
+ const extraParams = p.op.queryParams.filter((param) => !PAGINATION_PARAM_NAMES.has(param.name));
770
+ const needsWireSerializer = extraParams.some((param) => fieldName(param.name) !== wireFieldName(param.name));
771
+ return (
525
772
  p.plan.isPaginated &&
526
- (!optionsObjectParam(baselineMethodFor(service, p.method, ctx)) ||
773
+ (needsWireSerializer ||
774
+ !optionInfo ||
527
775
  /\bPaginationOptions\b/.test(
528
776
  preferredBaselineReturnType(ctx, baselineMethodFor(service, p.method, ctx)?.returnType) ?? '',
529
- )),
530
- );
777
+ ))
778
+ );
779
+ });
531
780
  const modelMap = new Map(ctx.spec.models.map((m) => [m.name, m]));
532
781
 
533
782
  // Collect models for imports — only include models that are actually used
534
783
  // in method signatures (not all union variants from the spec)
535
784
  const responseModels = new Set<string>();
785
+ const responseModelsForSignature = new Set<string>();
536
786
  const requestModels = new Set<string>();
537
787
  const requestModelsForSignature = new Set<string>();
538
788
  const paramEnums = new Set<string>();
539
789
  const paramModels = new Set<string>();
540
790
  const optionObjectTypes = new Set<string>();
791
+ const returnTypeImports = new Set<string>();
541
792
  const baselineResponseTypes = new Set<string>();
542
- for (const { op, plan } of plans) {
543
- const baselineMethod = baselineMethodFor(service, resolveMethodName(op, service, ctx), ctx);
544
- const existingOptions = optionsObjectParam(baselineMethod);
793
+ for (const { op, plan, method } of plans) {
794
+ const baselineMethod = baselineMethodFor(service, method, ctx);
795
+ const existingOptions = optionsObjectInfo(
796
+ service,
797
+ method,
798
+ op,
799
+ plan,
800
+ ctx,
801
+ baselineMethod,
802
+ lookupResolved(op, resolvedLookup),
803
+ );
545
804
  if (existingOptions) optionObjectTypes.add(existingOptions.type);
805
+ for (const typeName of operationOverrideFor(ctx, op)?.returnTypeImports ?? []) {
806
+ returnTypeImports.add(typeName);
807
+ }
546
808
 
547
- // Always collect param type refs for enums inline options interfaces
548
- // are generated for all methods (including baseline ones), so their
549
- // type dependencies must always be imported.
550
- const queryParams = plan.isPaginated
551
- ? op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name))
552
- : op.queryParams;
553
- for (const param of [...queryParams, ...op.pathParams]) {
554
- collectParamTypeRefs(param.type, paramEnums, paramModels);
809
+ // Collect param type refs only when params appear directly in this
810
+ // resource method. Options-object methods import a single options type;
811
+ // that generated interface owns its own enum/model imports.
812
+ if (!existingOptions) {
813
+ const queryParams = plan.isPaginated ? [] : op.queryParams;
814
+ for (const param of [...queryParams, ...op.pathParams]) {
815
+ collectParamTypeRefs(param.type, paramEnums, paramModels);
816
+ }
555
817
  }
556
818
 
557
819
  // Always collect imports for every rendered method. Earlier versions
@@ -581,8 +843,14 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
581
843
  }
582
844
  }
583
845
  responseModels.add(itemName);
846
+ responseModelsForSignature.add(itemName);
584
847
  } else if (plan.responseModelName) {
585
848
  responseModels.add(plan.responseModelName);
849
+ const override = operationOverrideFor(ctx, op);
850
+ const resolvedResponseName = resolveInterfaceName(plan.responseModelName, ctx);
851
+ if (!override?.returnType || override.returnType.includes(resolvedResponseName)) {
852
+ responseModelsForSignature.add(plan.responseModelName);
853
+ }
586
854
  }
587
855
  // Import request body model(s) — handles both single models and union variants.
588
856
  const bodyInfo = extractRequestBodyType(op, ctx);
@@ -613,7 +881,6 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
613
881
  // Also collect models referenced in wrapper param signatures (e.g.,
614
882
  // `redirect_uris: RedirectUriInput[]`) — otherwise the wrapper emits a
615
883
  // reference to a type it never imported.
616
- const resolvedLookup = buildResolvedLookup(ctx);
617
884
  for (const { op } of plans) {
618
885
  const resolved = lookupResolved(op, resolvedLookup);
619
886
  if (resolved) {
@@ -628,7 +895,7 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
628
895
  }
629
896
  }
630
897
 
631
- const allModels = new Set([...responseModels, ...requestModelsForSignature, ...paramModels]);
898
+ const allModels = new Set([...responseModelsForSignature, ...requestModelsForSignature, ...paramModels]);
632
899
 
633
900
  const lines: string[] = [];
634
901
 
@@ -641,16 +908,10 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
641
908
  lines.push("import { AutoPaginatable } from '../common/utils/pagination';");
642
909
  lines.push("import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize';");
643
910
  }
644
-
645
- // Paginated list options live in their own interface files so they're
646
- // picked up by the per-service barrel (and flow through to the root
647
- // package barrel). Import them here rather than declaring inline.
648
- for (const { op, plan, method } of plans) {
649
- if (!plan.isPaginated) continue;
650
- const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
651
- if (extraParams.length === 0) continue;
652
- const optionsName = paginatedOptionsName(method, resolvedName);
653
- lines.push(`import type { ${optionsName} } from './interfaces/${fileName(optionsName)}.interface';`);
911
+ const shouldEmitVaultCryptoHelpers =
912
+ serviceClass === 'Vault' && !ignoredMethodNames.has('encrypt') && !ignoredMethodNames.has('decrypt');
913
+ if (shouldEmitVaultCryptoHelpers) {
914
+ lines.push("import { base64ToUint8Array, uint8ArrayToBase64 } from '../common/utils/base64';");
654
915
  }
655
916
 
656
917
  // Check if any operation needs PostOptions (idempotent POST or custom encoding)
@@ -666,7 +927,18 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
666
927
  for (const optionType of optionObjectTypes) {
667
928
  if (importedTypeNames.has(optionType)) continue;
668
929
  importedTypeNames.add(optionType);
669
- lines.push(`import type { ${optionType} } from './interfaces/${fileName(optionType)}.interface';`);
930
+ const sourceFile = baselineTypeSourceFile(ctx, optionType);
931
+ const relPath = sourceFile
932
+ ? relativeImport(resourcePath, sourceFile)
933
+ : `./interfaces/${fileName(optionType)}.interface`;
934
+ lines.push(`import type { ${optionType} } from '${relPath}';`);
935
+ }
936
+ for (const typeName of returnTypeImports) {
937
+ if (importedTypeNames.has(typeName)) continue;
938
+ const sourceFile = baselineTypeSourceFile(ctx, typeName) ?? liveSurfaceInterfacePath(typeName);
939
+ if (!sourceFile) continue;
940
+ importedTypeNames.add(typeName);
941
+ lines.push(`import type { ${typeName} } from '${relativeImport(resourcePath, sourceFile)}';`);
670
942
  }
671
943
 
672
944
  // Compute model-to-service mapping for correct cross-service import paths
@@ -728,6 +1000,22 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
728
1000
  importedTypeNames.add(wireName);
729
1001
  }
730
1002
 
1003
+ for (const name of responseModels) {
1004
+ if (allModels.has(name)) continue;
1005
+ const resolved = resolveInterfaceName(name, ctx);
1006
+ if (!usedWireTypes.has(resolved)) continue;
1007
+ const wireName = wireInterfaceName(resolved);
1008
+ if (importedTypeNames.has(wireName)) continue;
1009
+ const modelDir = modelToService.get(name);
1010
+ const modelServiceDir = resolveDir(modelDir);
1011
+ const relPath =
1012
+ modelServiceDir === serviceDir
1013
+ ? `./interfaces/${fileName(name)}.interface`
1014
+ : `../${modelServiceDir}/interfaces/${fileName(name)}.interface`;
1015
+ lines.push(`import type { ${wireName} } from '${relPath}';`);
1016
+ importedTypeNames.add(wireName);
1017
+ }
1018
+
731
1019
  for (const name of baselineResponseTypes) {
732
1020
  if (importedTypeNames.has(name)) continue;
733
1021
  const wireName = wireInterfaceName(name);
@@ -836,10 +1124,13 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
836
1124
  // in separate files under `interfaces/` so the per-service barrel can
837
1125
  // re-export them; see the earlier import block at the top of the file.
838
1126
  for (const { op, plan, method } of plans) {
1127
+ const resolved = lookupResolved(op, resolvedLookup);
1128
+ const baselineMethod = baselineMethodFor(service, method, ctx);
1129
+ const optionInfo = optionsObjectInfo(service, method, op, plan, ctx, baselineMethod, resolved);
839
1130
  if (plan.isPaginated) {
840
1131
  const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
841
1132
  if (extraParams.length > 0) {
842
- const optionsName = paginatedOptionsName(method, resolvedName);
1133
+ const optionsName = optionInfo?.type ?? paginatedOptionsName(method, resolvedName);
843
1134
 
844
1135
  // When any extension param has a camelCase domain name that differs
845
1136
  // from its snake_case wire name, emit a serializer that translates
@@ -869,7 +1160,7 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
869
1160
  lines.push('');
870
1161
  }
871
1162
  }
872
- } else if (!plan.isPaginated && !plan.hasBody && !plan.isDelete && op.queryParams.length > 0) {
1163
+ } else if (!optionInfo && !plan.isPaginated && !plan.hasBody && !plan.isDelete && op.queryParams.length > 0) {
873
1164
  // Non-paginated GET or void methods with query params get a typed options interface
874
1165
  // instead of falling back to Record<string, unknown>.
875
1166
  // Filter out hidden params (defaults and inferFromClient fields)
@@ -898,6 +1189,16 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
898
1189
  }
899
1190
  }
900
1191
 
1192
+ if (shouldEmitVaultCryptoHelpers) {
1193
+ lines.push('export interface VaultEncryptedData {');
1194
+ lines.push(' ciphertext: string;');
1195
+ lines.push(' encryptedKeys: string;');
1196
+ lines.push(' iv: string;');
1197
+ lines.push(' tag: string;');
1198
+ lines.push('}');
1199
+ lines.push('');
1200
+ }
1201
+
901
1202
  // Resource class
902
1203
  if (service.description) {
903
1204
  lines.push(...docComment(service.description));
@@ -916,11 +1217,62 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
916
1217
  }
917
1218
  }
918
1219
 
1220
+ if (shouldEmitVaultCryptoHelpers) {
1221
+ lines.push('');
1222
+ lines.push(...renderVaultCryptoMethods());
1223
+ }
1224
+
919
1225
  lines.push('}');
920
1226
 
921
1227
  return { path: resourcePath, content: lines.join('\n'), skipIfExists: true };
922
1228
  }
923
1229
 
1230
+ function renderVaultCryptoMethods(): string[] {
1231
+ return [
1232
+ ' async encrypt(',
1233
+ ' plaintext: string,',
1234
+ ' context: Record<string, string>,',
1235
+ ' aad?: string,',
1236
+ ' ): Promise<VaultEncryptedData> {',
1237
+ ' const dataKeyPair = await this.createDataKey({ context });',
1238
+ ' const encodedPlaintext = new TextEncoder().encode(plaintext);',
1239
+ ' const encodedAad = aad ? new TextEncoder().encode(aad) : undefined;',
1240
+ ' const encrypted = await this.workos.getCryptoProvider().encrypt(',
1241
+ ' encodedPlaintext,',
1242
+ ' base64ToUint8Array(dataKeyPair.dataKey.key),',
1243
+ ' undefined,',
1244
+ ' encodedAad,',
1245
+ ' );',
1246
+ '',
1247
+ ' return {',
1248
+ ' ciphertext: uint8ArrayToBase64(encrypted.ciphertext),',
1249
+ ' encryptedKeys: dataKeyPair.encryptedKeys,',
1250
+ ' iv: uint8ArrayToBase64(encrypted.iv),',
1251
+ ' tag: uint8ArrayToBase64(encrypted.tag),',
1252
+ ' };',
1253
+ ' }',
1254
+ '',
1255
+ ' async decrypt(',
1256
+ ' encryptedData: VaultEncryptedData,',
1257
+ ' aad?: string,',
1258
+ ' ): Promise<string> {',
1259
+ ' const dataKey = await this.decryptDataKey({',
1260
+ ' keys: encryptedData.encryptedKeys,',
1261
+ ' });',
1262
+ ' const encodedAad = aad ? new TextEncoder().encode(aad) : undefined;',
1263
+ ' const decrypted = await this.workos.getCryptoProvider().decrypt(',
1264
+ ' base64ToUint8Array(encryptedData.ciphertext),',
1265
+ ' base64ToUint8Array(dataKey.key),',
1266
+ ' base64ToUint8Array(encryptedData.iv),',
1267
+ ' base64ToUint8Array(encryptedData.tag),',
1268
+ ' encodedAad,',
1269
+ ' );',
1270
+ '',
1271
+ ' return new TextDecoder().decode(decrypted);',
1272
+ ' }',
1273
+ ];
1274
+ }
1275
+
924
1276
  function renderMethod(
925
1277
  op: Operation,
926
1278
  plan: OperationPlan,
@@ -948,9 +1300,12 @@ function renderMethod(
948
1300
  // otherwise compute from what the render path will actually include.
949
1301
  const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
950
1302
  const baselineClassMethod = baselineMethodFor(service, method, ctx);
1303
+ const optionInfo = optionsObjectInfo(service, method, op, plan, ctx, baselineClassMethod, resolvedOp);
951
1304
  const overlayMethod = ctx.overlayLookup?.methodByOperation?.get(httpKey) ?? baselineClassMethod;
952
1305
  let validParamNames: Set<string> | null = null;
953
- if (overlayMethod) {
1306
+ if (optionInfo) {
1307
+ validParamNames = new Set(['options']);
1308
+ } else if (overlayMethod) {
954
1309
  validParamNames = new Set(overlayMethod.params.map((p) => p.name));
955
1310
  } else {
956
1311
  // Compute actual params based on render path to avoid documenting params
@@ -1185,7 +1540,20 @@ function renderMethod(
1185
1540
  }
1186
1541
  }
1187
1542
 
1188
- if (renderOptionsObjectMethod(lines, op, plan, method, service, ctx, modelMap, specEnumNames, baselineClassMethod)) {
1543
+ if (
1544
+ renderOptionsObjectMethod(
1545
+ lines,
1546
+ op,
1547
+ plan,
1548
+ method,
1549
+ service,
1550
+ ctx,
1551
+ modelMap,
1552
+ specEnumNames,
1553
+ baselineClassMethod,
1554
+ resolvedOp,
1555
+ )
1556
+ ) {
1189
1557
  return lines;
1190
1558
  }
1191
1559
 
@@ -1257,13 +1625,25 @@ function renderOptionsObjectMethod(
1257
1625
  modelMap: Map<string, Model>,
1258
1626
  specEnumNames: Set<string> | undefined,
1259
1627
  baselineMethod: BaselineMethod | undefined,
1628
+ resolvedOp: ResolvedOperation | undefined,
1260
1629
  ): boolean {
1261
- const optionParam = optionsObjectParam(baselineMethod);
1630
+ const optionParam = optionsObjectInfo(service, method, op, plan, ctx, baselineMethod, resolvedOp);
1262
1631
  if (!optionParam) return false;
1263
1632
 
1264
1633
  const responseModel = plan.responseModelName ? resolveInterfaceName(plan.responseModelName, ctx) : null;
1265
1634
  const pathBindings = buildOptionsObjectPathBindings(op, optionParam.type, ctx);
1266
1635
  const pathStr = buildPathStr(op, buildOptionsObjectPathParamMap(op, optionParam.type, ctx));
1636
+ const override = operationOverrideFor(ctx, op);
1637
+ const bodyFieldMap = override?.bodyFieldMap;
1638
+ const visibleQueryParams = visibleQueryParamsForOptions(op, plan, resolvedOp);
1639
+ const queryBindings = visibleQueryParams.map((param) => fieldName(param.name));
1640
+ const hasQuery =
1641
+ visibleQueryParams.length > 0 ||
1642
+ Object.keys(getOpDefaults(resolvedOp)).length > 0 ||
1643
+ getOpInferFromClient(resolvedOp).length > 0;
1644
+ const queryOptionsArg = hasQuery
1645
+ ? `, { query: ${renderQueryExprWithOptions(visibleQueryParams, optionParam.optional, resolvedOp)} }`
1646
+ : '';
1267
1647
 
1268
1648
  if (plan.isPaginated && op.pagination && op.httpMethod === 'get') {
1269
1649
  let itemRawName = op.pagination.itemType.kind === 'model' ? op.pagination.itemType.name : null;
@@ -1281,14 +1661,19 @@ function renderOptionsObjectMethod(
1281
1661
  const wireType = wireInterfaceName(itemType);
1282
1662
  const returnType =
1283
1663
  preferredBaselineReturnType(ctx, baselineMethod?.returnType) ?? `Promise<AutoPaginatable<${itemType}>>`;
1284
- lines.push(` async ${method}(options: ${optionParam.type}): ${returnType} {`);
1664
+ const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
1665
+ const needsWireSerializer = extraParams.some((p) => fieldName(p.name) !== wireFieldName(p.name));
1666
+ const listOptionsExpr = needsWireSerializer
1667
+ ? `options ? serialize${optionParam.type}(options) : undefined`
1668
+ : 'paginationOptions';
1669
+ lines.push(` async ${method}(${renderOptionsParam(optionParam)}): ${returnType} {`);
1285
1670
  renderOptionsObjectDestructure(lines, pathBindings, 'paginationOptions');
1286
1671
  lines.push(` return new AutoPaginatable(`);
1287
1672
  lines.push(` await fetchAndDeserialize<${wireType}, ${itemType}>(`);
1288
1673
  lines.push(` this.workos,`);
1289
1674
  lines.push(` ${pathStr},`);
1290
1675
  lines.push(` deserialize${itemType},`);
1291
- lines.push(` paginationOptions,`);
1676
+ lines.push(` ${listOptionsExpr},`);
1292
1677
  lines.push(` ),`);
1293
1678
  lines.push(` (params) =>`);
1294
1679
  lines.push(` fetchAndDeserialize<${wireType}, ${itemType}>(`);
@@ -1304,9 +1689,9 @@ function renderOptionsObjectMethod(
1304
1689
  }
1305
1690
 
1306
1691
  if (plan.isDelete && !plan.hasBody) {
1307
- lines.push(` async ${method}(options: ${optionParam.type}): Promise<void> {`);
1692
+ lines.push(` async ${method}(${renderOptionsParam(optionParam)}): Promise<void> {`);
1308
1693
  renderOptionsObjectDestructure(lines, pathBindings);
1309
- lines.push(` await this.workos.delete(${pathStr});`);
1694
+ lines.push(` await this.workos.delete(${pathStr}${queryOptionsArg});`);
1310
1695
  lines.push(' }');
1311
1696
  return true;
1312
1697
  }
@@ -1333,49 +1718,71 @@ function renderOptionsObjectMethod(
1333
1718
  }
1334
1719
 
1335
1720
  if (plan.isDelete) {
1336
- lines.push(` async ${method}(options: ${optionParam.type}): Promise<void> {`);
1337
- renderOptionsObjectDestructure(lines, pathBindings, 'payload');
1338
- lines.push(` await this.workos.deleteWithBody<${entityType}>(${pathStr}, ${bodyExpr});`);
1721
+ lines.push(` async ${method}(${renderOptionsParam(optionParam)}): Promise<void> {`);
1722
+ renderOptionsObjectDestructure(lines, [...pathBindings, ...queryBindings], 'payload');
1723
+ const bodyParam = renderOptionsObjectBodyFieldMap(lines, bodyFieldMap);
1724
+ bodyExpr = bodyArgExprWithParam(bodyExpr, bodyParam);
1725
+ lines.push(` await this.workos.deleteWithBody<${entityType}>(${pathStr}, ${bodyExpr}${queryOptionsArg});`);
1339
1726
  lines.push(' }');
1340
1727
  return true;
1341
1728
  }
1342
1729
 
1343
1730
  if (!responseModel) {
1344
- lines.push(` async ${method}(options: ${optionParam.type}): Promise<void> {`);
1345
- renderOptionsObjectDestructure(lines, pathBindings, 'payload');
1346
- lines.push(` await this.workos.${op.httpMethod}<void, ${entityType}>(${pathStr}, ${bodyExpr});`);
1731
+ lines.push(` async ${method}(${renderOptionsParam(optionParam)}): Promise<void> {`);
1732
+ renderOptionsObjectDestructure(lines, [...pathBindings, ...queryBindings], 'payload');
1733
+ const bodyParam = renderOptionsObjectBodyFieldMap(lines, bodyFieldMap);
1734
+ bodyExpr = bodyArgExprWithParam(bodyExpr, bodyParam);
1735
+ lines.push(
1736
+ ` await this.workos.${op.httpMethod}<void, ${entityType}>(${pathStr}, ${bodyExpr}${queryOptionsArg});`,
1737
+ );
1347
1738
  lines.push(' }');
1348
1739
  return true;
1349
1740
  }
1350
1741
 
1351
1742
  const returnType = plan.isArrayResponse ? `${responseModel}[]` : responseModel;
1743
+ const methodReturnType = override?.returnType ?? `Promise<${returnType}>`;
1352
1744
  const wireType = plan.isArrayResponse ? `${wireInterfaceName(responseModel)}[]` : wireInterfaceName(responseModel);
1353
1745
  const returnExpr = plan.isArrayResponse
1354
1746
  ? `data.map(deserialize${responseModel})`
1355
1747
  : `deserialize${responseModel}(data)`;
1748
+ const finalReturnExpr = override?.returnDataProperty ? `${returnExpr}.${override.returnDataProperty}` : returnExpr;
1356
1749
 
1357
- lines.push(` async ${method}(options: ${optionParam.type}): Promise<${returnType}> {`);
1358
- renderOptionsObjectDestructure(lines, pathBindings, 'payload');
1750
+ lines.push(` async ${method}(${renderOptionsParam(optionParam)}): ${methodReturnType} {`);
1751
+ renderOptionsObjectDestructure(lines, [...pathBindings, ...queryBindings], 'payload');
1752
+ const bodyParam = renderOptionsObjectBodyFieldMap(lines, bodyFieldMap);
1753
+ bodyExpr = bodyArgExprWithParam(bodyExpr, bodyParam);
1359
1754
  lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}, ${entityType}>(`);
1360
1755
  lines.push(` ${pathStr},`);
1361
- lines.push(` ${bodyExpr},`);
1756
+ lines.push(` ${bodyExpr}${queryOptionsArg},`);
1362
1757
  lines.push(' );');
1363
- lines.push(` return ${returnExpr};`);
1758
+ if (override?.returnExpression) {
1759
+ lines.push(` const result = ${returnExpr};`);
1760
+ lines.push(` return ${override.returnExpression};`);
1761
+ } else {
1762
+ lines.push(` return ${finalReturnExpr};`);
1763
+ }
1364
1764
  lines.push(' }');
1365
1765
  return true;
1366
1766
  }
1367
1767
 
1368
1768
  if (responseModel) {
1369
1769
  const returnType = plan.isArrayResponse ? `${responseModel}[]` : responseModel;
1770
+ const methodReturnType = override?.returnType ?? `Promise<${returnType}>`;
1370
1771
  const wireType = plan.isArrayResponse ? `${wireInterfaceName(responseModel)}[]` : wireInterfaceName(responseModel);
1371
1772
  const returnExpr = plan.isArrayResponse
1372
1773
  ? `data.map(deserialize${responseModel})`
1373
1774
  : `deserialize${responseModel}(data)`;
1775
+ const finalReturnExpr = override?.returnDataProperty ? `${returnExpr}.${override.returnDataProperty}` : returnExpr;
1374
1776
 
1375
- lines.push(` async ${method}(options: ${optionParam.type}): Promise<${returnType}> {`);
1777
+ lines.push(` async ${method}(${renderOptionsParam(optionParam)}): ${methodReturnType} {`);
1376
1778
  renderOptionsObjectDestructure(lines, pathBindings);
1377
- lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr});`);
1378
- lines.push(` return ${returnExpr};`);
1779
+ lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr}${queryOptionsArg});`);
1780
+ if (override?.returnExpression) {
1781
+ lines.push(` const result = ${returnExpr};`);
1782
+ lines.push(` return ${override.returnExpression};`);
1783
+ } else {
1784
+ lines.push(` return ${finalReturnExpr};`);
1785
+ }
1379
1786
  lines.push(' }');
1380
1787
  return true;
1381
1788
  }
@@ -1393,6 +1800,23 @@ function renderOptionsObjectDestructure(lines: string[], pathBindings: string[],
1393
1800
  }
1394
1801
  }
1395
1802
 
1803
+ function renderOptionsObjectBodyFieldMap(lines: string[], bodyFieldMap: Record<string, string> | undefined): string {
1804
+ const entries = Object.entries(bodyFieldMap ?? {});
1805
+ if (entries.length === 0) return 'payload';
1806
+
1807
+ lines.push(' const requestPayload = {');
1808
+ lines.push(' ...payload,');
1809
+ for (const [source, target] of entries) {
1810
+ lines.push(` ${target}: payload.${source},`);
1811
+ }
1812
+ lines.push(' };');
1813
+ return 'requestPayload';
1814
+ }
1815
+
1816
+ function bodyArgExprWithParam(bodyExpr: string, paramName: string): string {
1817
+ return paramName === 'payload' ? bodyExpr : bodyExpr.replace(/\bpayload\b/g, paramName);
1818
+ }
1819
+
1396
1820
  function buildOptionsObjectPathBindings(op: Operation, optionType: string, ctx: EmitterContext): string[] {
1397
1821
  // Return resolved SDK field names directly — the URL template uses these
1398
1822
  // names too (via the param-name map threaded into buildNodePathExpression),
@@ -1862,6 +2286,32 @@ function renderQueryExpr(queryParams: { name: string; required: boolean }[]): st
1862
2286
  return `options ? { ${parts.join(', ')} } : undefined`;
1863
2287
  }
1864
2288
 
2289
+ function renderQueryExprWithOptions(
2290
+ queryParams: { name: string; required: boolean }[],
2291
+ optional: boolean,
2292
+ resolvedOp?: ResolvedOperation,
2293
+ ): string {
2294
+ const parts: string[] = [];
2295
+ for (const param of queryParams) {
2296
+ const camel = fieldName(param.name);
2297
+ const snake = wireFieldName(param.name);
2298
+ const access = optional ? `options?.${camel}` : `options.${camel}`;
2299
+ if (param.required) {
2300
+ parts.push(`${snake}: ${access}`);
2301
+ } else {
2302
+ parts.push(`...(${access} !== undefined && { ${snake}: ${access} })`);
2303
+ }
2304
+ }
2305
+ for (const [key, value] of Object.entries(getOpDefaults(resolvedOp))) {
2306
+ parts.push(`${key}: ${tsLiteral(value)}`);
2307
+ }
2308
+ for (const field of getOpInferFromClient(resolvedOp)) {
2309
+ parts.push(`${field}: ${clientFieldExpression(field)}`);
2310
+ }
2311
+ if (parts.length === 0) return optional ? 'options' : '{}';
2312
+ return `{ ${parts.join(', ')} }`;
2313
+ }
2314
+
1865
2315
  function buildPathStr(op: Operation, paramNameMap?: Map<string, string>): string {
1866
2316
  return buildNodePathExpression(op.path, paramNameMap);
1867
2317
  }