@workos/oagen-emitters 0.14.3 → 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.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +19 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{plugin-D0qLBiGv.mjs → plugin-CO4RFgAW.mjs} +983 -270
- package/dist/plugin-CO4RFgAW.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +9 -9
- package/renovate.json +1 -61
- package/src/go/client.ts +1 -1
- package/src/go/enums.ts +77 -0
- package/src/kotlin/enums.ts +11 -4
- package/src/node/client.ts +119 -2
- package/src/node/discriminated-models.ts +20 -28
- package/src/node/field-plan.ts +64 -8
- package/src/node/index.ts +59 -3
- package/src/node/models.ts +73 -30
- package/src/node/naming.ts +14 -1
- package/src/node/node-overrides.ts +4 -37
- package/src/node/options.ts +29 -1
- package/src/node/resources.ts +533 -83
- package/src/node/tests.ts +108 -7
- package/src/php/fixtures.ts +4 -1
- package/src/php/models.ts +3 -1
- package/src/php/resources.ts +40 -11
- package/src/php/tests.ts +22 -12
- package/src/python/client.ts +0 -8
- package/src/python/enums.ts +41 -15
- package/src/python/fixtures.ts +23 -7
- package/src/python/models.ts +26 -5
- package/src/python/resources.ts +71 -3
- package/src/python/tests.ts +70 -12
- package/src/python/wrappers.ts +25 -4
- package/src/ruby/client.ts +0 -1
- package/src/ruby/rbi.ts +12 -6
- package/src/rust/resources.ts +10 -7
- package/src/shared/non-spec-services.ts +0 -5
- package/test/go/enums.test.ts +24 -0
- package/test/node/resources.test.ts +11 -1
- package/test/node/tests.test.ts +3 -3
- package/test/php/client.test.ts +0 -1
- package/test/php/resources.test.ts +50 -0
- package/test/rust/resources.test.ts +9 -0
- package/dist/plugin-D0qLBiGv.mjs.map +0 -1
package/src/node/resources.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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:
|
|
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
|
|
321
|
-
*
|
|
322
|
-
*
|
|
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
|
|
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
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
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
|
|
348
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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 (
|
|
359
|
-
if (
|
|
360
|
-
|
|
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(...
|
|
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
|
|
524
|
-
|
|
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
|
-
(
|
|
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,
|
|
544
|
-
const existingOptions =
|
|
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
|
-
//
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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([...
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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(`
|
|
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}(
|
|
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}(
|
|
1337
|
-
renderOptionsObjectDestructure(lines, pathBindings, 'payload');
|
|
1338
|
-
|
|
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}(
|
|
1345
|
-
renderOptionsObjectDestructure(lines, pathBindings, 'payload');
|
|
1346
|
-
|
|
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}(
|
|
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
|
-
|
|
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}(
|
|
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
|
-
|
|
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
|
}
|