@workos/oagen-emitters 0.14.4 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.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-BGVaMGqe.mjs → plugin-C2Hp2Vs2.mjs} +1039 -274
- package/dist/plugin-C2Hp2Vs2.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +7 -7
- 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 +158 -2
- package/src/node/discriminated-models.ts +68 -24
- 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 +553 -89
- 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/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-BGVaMGqe.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,22 +163,157 @@ 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 {
|
|
174
|
-
|
|
314
|
+
// Match both AutoPaginatable<T> and the legacy List<T> pattern so baseline
|
|
315
|
+
// item types are extracted even when the hand-written code predates AutoPaginatable.
|
|
316
|
+
return returnType?.match(/\b(?:AutoPaginatable|List)<\s*([A-Za-z_$][\w$]*)/)?.[1];
|
|
175
317
|
}
|
|
176
318
|
|
|
177
319
|
function baselineTypeSourceFile(ctx: EmitterContext, typeName: string): string | undefined {
|
|
@@ -207,9 +349,15 @@ function preferredBaselineTypeName(ctx: EmitterContext, typeName: string | undef
|
|
|
207
349
|
}
|
|
208
350
|
|
|
209
351
|
function preferredBaselineReturnType(ctx: EmitterContext, returnType: string | undefined): string | undefined {
|
|
352
|
+
if (!returnType) return undefined;
|
|
353
|
+
// Only preserve baseline return types that already use AutoPaginatable.
|
|
354
|
+
// Legacy patterns like List<T> can't be used as-is since the generated
|
|
355
|
+
// method body returns new AutoPaginatable(...).
|
|
356
|
+
if (!/\bAutoPaginatable\b/.test(returnType)) return undefined;
|
|
210
357
|
const itemType = autoPaginatableItemType(returnType);
|
|
358
|
+
if (!itemType) return undefined;
|
|
211
359
|
const preferred = preferredBaselineTypeName(ctx, itemType);
|
|
212
|
-
if (!
|
|
360
|
+
if (!preferred || preferred === itemType) return returnType;
|
|
213
361
|
return returnType.replace(new RegExp(`\\b${itemType}\\b`, 'g'), preferred);
|
|
214
362
|
}
|
|
215
363
|
|
|
@@ -317,21 +465,14 @@ function deduplicateMethodNames(
|
|
|
317
465
|
}
|
|
318
466
|
|
|
319
467
|
/**
|
|
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.
|
|
468
|
+
* Emit one interface/type file per generated options-object operation.
|
|
469
|
+
* Placing the options type under `interfaces/` lets the per-service barrel
|
|
470
|
+
* pick it up via `export * from './interfaces'`.
|
|
326
471
|
*/
|
|
327
|
-
function
|
|
328
|
-
service: Service,
|
|
329
|
-
ctx: EmitterContext,
|
|
330
|
-
specEnumNames: Set<string>,
|
|
331
|
-
): GeneratedFile[] {
|
|
472
|
+
function generateOptionsInterfaces(service: Service, ctx: EmitterContext, specEnumNames: Set<string>): GeneratedFile[] {
|
|
332
473
|
const files: GeneratedFile[] = [];
|
|
333
|
-
const resolvedName = resolveResourceClassName(service, ctx);
|
|
334
474
|
const serviceDir = resolveResourceDir(service, ctx);
|
|
475
|
+
const resolvedLookup = buildResolvedLookup(ctx);
|
|
335
476
|
|
|
336
477
|
const plans = service.operations.map((op) => ({
|
|
337
478
|
op,
|
|
@@ -340,28 +481,125 @@ function generatePaginatedOptionsInterfaces(
|
|
|
340
481
|
}));
|
|
341
482
|
|
|
342
483
|
for (const { op, plan, method } of plans) {
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
484
|
+
const resolvedOp = lookupResolved(op, resolvedLookup);
|
|
485
|
+
const baselineMethod = baselineMethodFor(service, method, ctx);
|
|
486
|
+
const optionInfo = optionsObjectInfo(service, method, op, plan, ctx, baselineMethod, resolvedOp);
|
|
487
|
+
if (!optionInfo?.generated) continue;
|
|
488
|
+
if (baselineTypeSourceFile(ctx, optionInfo.type)) continue;
|
|
489
|
+
|
|
490
|
+
const optionsName = optionInfo.type;
|
|
491
|
+
const optionFileStem = `${fileName(optionsName)}.interface`;
|
|
492
|
+
const filePath = `src/${serviceDir}/interfaces/${optionFileStem}.ts`;
|
|
493
|
+
if (!liveSurfaceHasFile(filePath) || existingInterfaceBarrelExports(ctx, serviceDir, optionFileStem)) {
|
|
494
|
+
recordGeneratedOptionInterface(ctx, serviceDir, optionFileStem, optionsName);
|
|
495
|
+
}
|
|
346
496
|
|
|
347
|
-
const
|
|
348
|
-
const
|
|
497
|
+
const optEnums = new Set<string>();
|
|
498
|
+
const optModels = new Set<string>();
|
|
499
|
+
for (const param of [...op.pathParams, ...visibleQueryParamsForOptions(op, plan, resolvedOp)]) {
|
|
500
|
+
collectParamTypeRefs(param.type, optEnums, optModels);
|
|
501
|
+
}
|
|
502
|
+
const bodyInfo = extractRequestBodyType(op, ctx);
|
|
503
|
+
if (bodyInfo?.kind === 'model') {
|
|
504
|
+
const bodyModel = ctx.spec.models.find((m) => m.name === bodyInfo.name);
|
|
505
|
+
if (bodyModel) {
|
|
506
|
+
for (const field of bodyModel.fields) {
|
|
507
|
+
collectParamTypeRefs(field.type, optEnums, optModels);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
} else if (bodyInfo?.kind === 'union') {
|
|
511
|
+
for (const name of bodyInfo.modelNames) {
|
|
512
|
+
optModels.add(name);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
349
515
|
|
|
350
516
|
const lines: string[] = [];
|
|
351
|
-
|
|
517
|
+
if (plan.isPaginated) {
|
|
518
|
+
lines.push("import type { PaginationOptions } from '../../common/interfaces/pagination-options.interface';");
|
|
519
|
+
}
|
|
520
|
+
const { modelToService, resolveDir } = createServiceDirResolver(ctx.spec.models, ctx.spec.services, ctx);
|
|
521
|
+
if (optEnums.size > 0) {
|
|
522
|
+
const enumToService = assignEnumsToServices(ctx.spec.enums, ctx.spec.services, ctx.spec.models, ctx);
|
|
523
|
+
for (const name of optEnums) {
|
|
524
|
+
if (!specEnumNames.has(name)) continue;
|
|
525
|
+
if (isInlineEnum(name)) continue;
|
|
526
|
+
const enumDir = resolveDir(enumToService.get(name));
|
|
527
|
+
const relPath =
|
|
528
|
+
enumDir === serviceDir
|
|
529
|
+
? `./${fileName(name)}.interface`
|
|
530
|
+
: `../../${enumDir}/interfaces/${fileName(name)}.interface`;
|
|
531
|
+
lines.push(`import type { ${name} } from '${relPath}';`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
for (const name of optModels) {
|
|
535
|
+
const modelDir = resolveDir(modelToService.get(name));
|
|
536
|
+
const relPath =
|
|
537
|
+
modelDir === serviceDir
|
|
538
|
+
? `./${fileName(name)}.interface`
|
|
539
|
+
: `../../${modelDir}/interfaces/${fileName(name)}.interface`;
|
|
540
|
+
lines.push(`import type { ${resolveInterfaceName(name, ctx)} } from '${relPath}';`);
|
|
541
|
+
}
|
|
352
542
|
lines.push('');
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
543
|
+
|
|
544
|
+
const headerParts: string[] = [];
|
|
545
|
+
const pushField = (name: string, required: boolean, type: string, description?: string, deprecated?: boolean) => {
|
|
546
|
+
const opt = !required ? '?' : '';
|
|
547
|
+
if (description || deprecated) {
|
|
357
548
|
const parts: string[] = [];
|
|
358
|
-
if (
|
|
359
|
-
if (
|
|
360
|
-
|
|
549
|
+
if (description) parts.push(description);
|
|
550
|
+
if (deprecated) parts.push('@deprecated');
|
|
551
|
+
headerParts.push(...docComment(parts.join('\n'), 2));
|
|
552
|
+
}
|
|
553
|
+
headerParts.push(` ${name}${opt}: ${type};`);
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
for (const param of op.pathParams) {
|
|
557
|
+
pushField(
|
|
558
|
+
fieldName(param.name),
|
|
559
|
+
true,
|
|
560
|
+
mapParamType(param.type, specEnumNames),
|
|
561
|
+
param.description,
|
|
562
|
+
param.deprecated,
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
for (const param of visibleQueryParamsForOptions(op, plan, resolvedOp)) {
|
|
566
|
+
pushField(
|
|
567
|
+
fieldName(param.name),
|
|
568
|
+
param.required,
|
|
569
|
+
mapParamType(param.type, specEnumNames),
|
|
570
|
+
param.description,
|
|
571
|
+
param.deprecated,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (bodyInfo?.kind === 'model') {
|
|
576
|
+
const bodyModel = ctx.spec.models.find((m) => m.name === bodyInfo.name);
|
|
577
|
+
if (bodyModel) {
|
|
578
|
+
for (const field of bodyModel.fields) {
|
|
579
|
+
pushField(
|
|
580
|
+
fieldName(field.name),
|
|
581
|
+
field.required,
|
|
582
|
+
mapParamType(field.type, specEnumNames),
|
|
583
|
+
field.description,
|
|
584
|
+
field.deprecated,
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
lines.push(`export interface ${optionsName}${plan.isPaginated ? ' extends PaginationOptions' : ''} {`);
|
|
589
|
+
lines.push(...headerParts);
|
|
590
|
+
lines.push('}');
|
|
591
|
+
} else if (bodyInfo?.kind === 'union') {
|
|
592
|
+
const baseType = headerParts.length > 0 ? `{\n${headerParts.join('\n')}\n}` : '{}';
|
|
593
|
+
lines.push(`export type ${optionsName} = ${baseType} & (${bodyInfo.typeStr});`);
|
|
594
|
+
} else {
|
|
595
|
+
if (plan.isPaginated && headerParts.length === 0) {
|
|
596
|
+
lines.push(`export type ${optionsName} = PaginationOptions;`);
|
|
597
|
+
} else {
|
|
598
|
+
lines.push(`export interface ${optionsName}${plan.isPaginated ? ' extends PaginationOptions' : ''} {`);
|
|
599
|
+
lines.push(...headerParts);
|
|
600
|
+
lines.push('}');
|
|
361
601
|
}
|
|
362
|
-
lines.push(` ${fieldName(param.name)}${opt}: ${mapParamType(param.type, specEnumNames)};`);
|
|
363
602
|
}
|
|
364
|
-
lines.push('}');
|
|
365
603
|
|
|
366
604
|
files.push({
|
|
367
605
|
path: filePath,
|
|
@@ -435,7 +673,7 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
435
673
|
const isOwnedService = isNodeOwnedService(ctx, service.name, resolveResourceClassName(service, ctx));
|
|
436
674
|
if (!isOwnedService && isServiceCoveredByExisting(service, ctx) && !hasMethodsAbsentFromBaseline(service, ctx))
|
|
437
675
|
continue;
|
|
438
|
-
files.push(...
|
|
676
|
+
files.push(...generateOptionsInterfaces(service, ctx, topLevelEnumNames));
|
|
439
677
|
}
|
|
440
678
|
|
|
441
679
|
return files;
|
|
@@ -504,8 +742,12 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
504
742
|
// Done after dedup + sort so the rendered methods keep their stable names.
|
|
505
743
|
// Imports below filter through `plans`, so they automatically narrow to
|
|
506
744
|
// the kept operations only — no orphan imports.
|
|
745
|
+
const ignoredMethodNames = ignoredResourceMethodNames(ctx, resourcePath);
|
|
507
746
|
const baselineMethodNames = new Set(Object.keys(ctx.apiSurface?.classes?.[serviceClass]?.methods ?? {}));
|
|
508
747
|
const planCountBeforeFilter = plans.length;
|
|
748
|
+
if (ignoredMethodNames.size > 0) {
|
|
749
|
+
plans = plans.filter((p) => !ignoredMethodNames.has(p.method));
|
|
750
|
+
}
|
|
509
751
|
if (!isNodeOwnedService(ctx, service.name, serviceClass) && baselineMethodNames.size > 0) {
|
|
510
752
|
plans = plans.filter((p) => !baselineMethodNames.has(p.method));
|
|
511
753
|
}
|
|
@@ -520,38 +762,66 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
520
762
|
}
|
|
521
763
|
|
|
522
764
|
const hasPaginated = plans.some((p) => p.plan.isPaginated);
|
|
523
|
-
const
|
|
524
|
-
|
|
765
|
+
const resolvedLookup = buildResolvedLookup(ctx);
|
|
766
|
+
const needsPaginationOptionsImport = plans.some((p) => {
|
|
767
|
+
const baseline = baselineMethodFor(service, p.method, ctx);
|
|
768
|
+
const optionInfo = optionsObjectInfo(
|
|
769
|
+
service,
|
|
770
|
+
p.method,
|
|
771
|
+
p.op,
|
|
772
|
+
p.plan,
|
|
773
|
+
ctx,
|
|
774
|
+
baseline,
|
|
775
|
+
lookupResolved(p.op, resolvedLookup),
|
|
776
|
+
);
|
|
777
|
+
const extraParams = p.op.queryParams.filter((param) => !PAGINATION_PARAM_NAMES.has(param.name));
|
|
778
|
+
const needsWireSerializer = extraParams.some((param) => fieldName(param.name) !== wireFieldName(param.name));
|
|
779
|
+
return (
|
|
525
780
|
p.plan.isPaginated &&
|
|
526
|
-
(
|
|
781
|
+
(needsWireSerializer ||
|
|
782
|
+
!optionInfo ||
|
|
527
783
|
/\bPaginationOptions\b/.test(
|
|
528
784
|
preferredBaselineReturnType(ctx, baselineMethodFor(service, p.method, ctx)?.returnType) ?? '',
|
|
529
|
-
))
|
|
530
|
-
|
|
785
|
+
))
|
|
786
|
+
);
|
|
787
|
+
});
|
|
531
788
|
const modelMap = new Map(ctx.spec.models.map((m) => [m.name, m]));
|
|
532
789
|
|
|
533
790
|
// Collect models for imports — only include models that are actually used
|
|
534
791
|
// in method signatures (not all union variants from the spec)
|
|
535
792
|
const responseModels = new Set<string>();
|
|
793
|
+
const responseModelsForSignature = new Set<string>();
|
|
536
794
|
const requestModels = new Set<string>();
|
|
537
795
|
const requestModelsForSignature = new Set<string>();
|
|
538
796
|
const paramEnums = new Set<string>();
|
|
539
797
|
const paramModels = new Set<string>();
|
|
540
798
|
const optionObjectTypes = new Set<string>();
|
|
799
|
+
const returnTypeImports = new Set<string>();
|
|
541
800
|
const baselineResponseTypes = new Set<string>();
|
|
542
|
-
for (const { op, plan } of plans) {
|
|
543
|
-
const baselineMethod = baselineMethodFor(service,
|
|
544
|
-
const existingOptions =
|
|
801
|
+
for (const { op, plan, method } of plans) {
|
|
802
|
+
const baselineMethod = baselineMethodFor(service, method, ctx);
|
|
803
|
+
const existingOptions = optionsObjectInfo(
|
|
804
|
+
service,
|
|
805
|
+
method,
|
|
806
|
+
op,
|
|
807
|
+
plan,
|
|
808
|
+
ctx,
|
|
809
|
+
baselineMethod,
|
|
810
|
+
lookupResolved(op, resolvedLookup),
|
|
811
|
+
);
|
|
545
812
|
if (existingOptions) optionObjectTypes.add(existingOptions.type);
|
|
813
|
+
for (const typeName of operationOverrideFor(ctx, op)?.returnTypeImports ?? []) {
|
|
814
|
+
returnTypeImports.add(typeName);
|
|
815
|
+
}
|
|
546
816
|
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
817
|
+
// Collect param type refs only when params appear directly in this
|
|
818
|
+
// resource method. Options-object methods import a single options type;
|
|
819
|
+
// that generated interface owns its own enum/model imports.
|
|
820
|
+
if (!existingOptions) {
|
|
821
|
+
const queryParams = plan.isPaginated ? [] : op.queryParams;
|
|
822
|
+
for (const param of [...queryParams, ...op.pathParams]) {
|
|
823
|
+
collectParamTypeRefs(param.type, paramEnums, paramModels);
|
|
824
|
+
}
|
|
555
825
|
}
|
|
556
826
|
|
|
557
827
|
// Always collect imports for every rendered method. Earlier versions
|
|
@@ -581,8 +851,14 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
581
851
|
}
|
|
582
852
|
}
|
|
583
853
|
responseModels.add(itemName);
|
|
854
|
+
responseModelsForSignature.add(itemName);
|
|
584
855
|
} else if (plan.responseModelName) {
|
|
585
856
|
responseModels.add(plan.responseModelName);
|
|
857
|
+
const override = operationOverrideFor(ctx, op);
|
|
858
|
+
const resolvedResponseName = resolveInterfaceName(plan.responseModelName, ctx);
|
|
859
|
+
if (!override?.returnType || override.returnType.includes(resolvedResponseName)) {
|
|
860
|
+
responseModelsForSignature.add(plan.responseModelName);
|
|
861
|
+
}
|
|
586
862
|
}
|
|
587
863
|
// Import request body model(s) — handles both single models and union variants.
|
|
588
864
|
const bodyInfo = extractRequestBodyType(op, ctx);
|
|
@@ -613,7 +889,6 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
613
889
|
// Also collect models referenced in wrapper param signatures (e.g.,
|
|
614
890
|
// `redirect_uris: RedirectUriInput[]`) — otherwise the wrapper emits a
|
|
615
891
|
// reference to a type it never imported.
|
|
616
|
-
const resolvedLookup = buildResolvedLookup(ctx);
|
|
617
892
|
for (const { op } of plans) {
|
|
618
893
|
const resolved = lookupResolved(op, resolvedLookup);
|
|
619
894
|
if (resolved) {
|
|
@@ -628,7 +903,7 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
628
903
|
}
|
|
629
904
|
}
|
|
630
905
|
|
|
631
|
-
const allModels = new Set([...
|
|
906
|
+
const allModels = new Set([...responseModelsForSignature, ...requestModelsForSignature, ...paramModels]);
|
|
632
907
|
|
|
633
908
|
const lines: string[] = [];
|
|
634
909
|
|
|
@@ -641,16 +916,10 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
641
916
|
lines.push("import { AutoPaginatable } from '../common/utils/pagination';");
|
|
642
917
|
lines.push("import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize';");
|
|
643
918
|
}
|
|
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';`);
|
|
919
|
+
const shouldEmitVaultCryptoHelpers =
|
|
920
|
+
serviceClass === 'Vault' && !ignoredMethodNames.has('encrypt') && !ignoredMethodNames.has('decrypt');
|
|
921
|
+
if (shouldEmitVaultCryptoHelpers) {
|
|
922
|
+
lines.push("import { base64ToUint8Array, uint8ArrayToBase64 } from '../common/utils/base64';");
|
|
654
923
|
}
|
|
655
924
|
|
|
656
925
|
// Check if any operation needs PostOptions (idempotent POST or custom encoding)
|
|
@@ -666,7 +935,18 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
666
935
|
for (const optionType of optionObjectTypes) {
|
|
667
936
|
if (importedTypeNames.has(optionType)) continue;
|
|
668
937
|
importedTypeNames.add(optionType);
|
|
669
|
-
|
|
938
|
+
const sourceFile = baselineTypeSourceFile(ctx, optionType);
|
|
939
|
+
const relPath = sourceFile
|
|
940
|
+
? relativeImport(resourcePath, sourceFile)
|
|
941
|
+
: `./interfaces/${fileName(optionType)}.interface`;
|
|
942
|
+
lines.push(`import type { ${optionType} } from '${relPath}';`);
|
|
943
|
+
}
|
|
944
|
+
for (const typeName of returnTypeImports) {
|
|
945
|
+
if (importedTypeNames.has(typeName)) continue;
|
|
946
|
+
const sourceFile = baselineTypeSourceFile(ctx, typeName) ?? liveSurfaceInterfacePath(typeName);
|
|
947
|
+
if (!sourceFile) continue;
|
|
948
|
+
importedTypeNames.add(typeName);
|
|
949
|
+
lines.push(`import type { ${typeName} } from '${relativeImport(resourcePath, sourceFile)}';`);
|
|
670
950
|
}
|
|
671
951
|
|
|
672
952
|
// Compute model-to-service mapping for correct cross-service import paths
|
|
@@ -728,6 +1008,22 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
728
1008
|
importedTypeNames.add(wireName);
|
|
729
1009
|
}
|
|
730
1010
|
|
|
1011
|
+
for (const name of responseModels) {
|
|
1012
|
+
if (allModels.has(name)) continue;
|
|
1013
|
+
const resolved = resolveInterfaceName(name, ctx);
|
|
1014
|
+
if (!usedWireTypes.has(resolved)) continue;
|
|
1015
|
+
const wireName = wireInterfaceName(resolved);
|
|
1016
|
+
if (importedTypeNames.has(wireName)) continue;
|
|
1017
|
+
const modelDir = modelToService.get(name);
|
|
1018
|
+
const modelServiceDir = resolveDir(modelDir);
|
|
1019
|
+
const relPath =
|
|
1020
|
+
modelServiceDir === serviceDir
|
|
1021
|
+
? `./interfaces/${fileName(name)}.interface`
|
|
1022
|
+
: `../${modelServiceDir}/interfaces/${fileName(name)}.interface`;
|
|
1023
|
+
lines.push(`import type { ${wireName} } from '${relPath}';`);
|
|
1024
|
+
importedTypeNames.add(wireName);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
731
1027
|
for (const name of baselineResponseTypes) {
|
|
732
1028
|
if (importedTypeNames.has(name)) continue;
|
|
733
1029
|
const wireName = wireInterfaceName(name);
|
|
@@ -836,10 +1132,13 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
836
1132
|
// in separate files under `interfaces/` so the per-service barrel can
|
|
837
1133
|
// re-export them; see the earlier import block at the top of the file.
|
|
838
1134
|
for (const { op, plan, method } of plans) {
|
|
1135
|
+
const resolved = lookupResolved(op, resolvedLookup);
|
|
1136
|
+
const baselineMethod = baselineMethodFor(service, method, ctx);
|
|
1137
|
+
const optionInfo = optionsObjectInfo(service, method, op, plan, ctx, baselineMethod, resolved);
|
|
839
1138
|
if (plan.isPaginated) {
|
|
840
1139
|
const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
|
|
841
1140
|
if (extraParams.length > 0) {
|
|
842
|
-
const optionsName = paginatedOptionsName(method, resolvedName);
|
|
1141
|
+
const optionsName = optionInfo?.type ?? paginatedOptionsName(method, resolvedName);
|
|
843
1142
|
|
|
844
1143
|
// When any extension param has a camelCase domain name that differs
|
|
845
1144
|
// from its snake_case wire name, emit a serializer that translates
|
|
@@ -855,7 +1154,11 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
855
1154
|
// extension fields land on top and the camelCase keys don't also
|
|
856
1155
|
// leak into the query string.
|
|
857
1156
|
lines.push(' const wire: Record<string, unknown> = {');
|
|
1157
|
+
const baselineFields = (
|
|
1158
|
+
ctx.apiSurface?.interfaces as Record<string, { fields?: Record<string, unknown> }> | undefined
|
|
1159
|
+
)?.[optionsName]?.fields;
|
|
858
1160
|
for (const p of PAGINATION_PARAM_NAMES) {
|
|
1161
|
+
if (baselineFields && !(p in baselineFields) && !baselineFields[toCamelCase(p)]) continue;
|
|
859
1162
|
lines.push(` ${p}: options.${p},`);
|
|
860
1163
|
}
|
|
861
1164
|
lines.push(' };');
|
|
@@ -869,7 +1172,7 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
869
1172
|
lines.push('');
|
|
870
1173
|
}
|
|
871
1174
|
}
|
|
872
|
-
} else if (!plan.isPaginated && !plan.hasBody && !plan.isDelete && op.queryParams.length > 0) {
|
|
1175
|
+
} else if (!optionInfo && !plan.isPaginated && !plan.hasBody && !plan.isDelete && op.queryParams.length > 0) {
|
|
873
1176
|
// Non-paginated GET or void methods with query params get a typed options interface
|
|
874
1177
|
// instead of falling back to Record<string, unknown>.
|
|
875
1178
|
// Filter out hidden params (defaults and inferFromClient fields)
|
|
@@ -898,6 +1201,16 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
898
1201
|
}
|
|
899
1202
|
}
|
|
900
1203
|
|
|
1204
|
+
if (shouldEmitVaultCryptoHelpers) {
|
|
1205
|
+
lines.push('export interface VaultEncryptedData {');
|
|
1206
|
+
lines.push(' ciphertext: string;');
|
|
1207
|
+
lines.push(' encryptedKeys: string;');
|
|
1208
|
+
lines.push(' iv: string;');
|
|
1209
|
+
lines.push(' tag: string;');
|
|
1210
|
+
lines.push('}');
|
|
1211
|
+
lines.push('');
|
|
1212
|
+
}
|
|
1213
|
+
|
|
901
1214
|
// Resource class
|
|
902
1215
|
if (service.description) {
|
|
903
1216
|
lines.push(...docComment(service.description));
|
|
@@ -916,11 +1229,62 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
916
1229
|
}
|
|
917
1230
|
}
|
|
918
1231
|
|
|
1232
|
+
if (shouldEmitVaultCryptoHelpers) {
|
|
1233
|
+
lines.push('');
|
|
1234
|
+
lines.push(...renderVaultCryptoMethods());
|
|
1235
|
+
}
|
|
1236
|
+
|
|
919
1237
|
lines.push('}');
|
|
920
1238
|
|
|
921
1239
|
return { path: resourcePath, content: lines.join('\n'), skipIfExists: true };
|
|
922
1240
|
}
|
|
923
1241
|
|
|
1242
|
+
function renderVaultCryptoMethods(): string[] {
|
|
1243
|
+
return [
|
|
1244
|
+
' async encrypt(',
|
|
1245
|
+
' plaintext: string,',
|
|
1246
|
+
' context: Record<string, string>,',
|
|
1247
|
+
' aad?: string,',
|
|
1248
|
+
' ): Promise<VaultEncryptedData> {',
|
|
1249
|
+
' const dataKeyPair = await this.createDataKey({ context });',
|
|
1250
|
+
' const encodedPlaintext = new TextEncoder().encode(plaintext);',
|
|
1251
|
+
' const encodedAad = aad ? new TextEncoder().encode(aad) : undefined;',
|
|
1252
|
+
' const encrypted = await this.workos.getCryptoProvider().encrypt(',
|
|
1253
|
+
' encodedPlaintext,',
|
|
1254
|
+
' base64ToUint8Array(dataKeyPair.dataKey.key),',
|
|
1255
|
+
' undefined,',
|
|
1256
|
+
' encodedAad,',
|
|
1257
|
+
' );',
|
|
1258
|
+
'',
|
|
1259
|
+
' return {',
|
|
1260
|
+
' ciphertext: uint8ArrayToBase64(encrypted.ciphertext),',
|
|
1261
|
+
' encryptedKeys: dataKeyPair.encryptedKeys,',
|
|
1262
|
+
' iv: uint8ArrayToBase64(encrypted.iv),',
|
|
1263
|
+
' tag: uint8ArrayToBase64(encrypted.tag),',
|
|
1264
|
+
' };',
|
|
1265
|
+
' }',
|
|
1266
|
+
'',
|
|
1267
|
+
' async decrypt(',
|
|
1268
|
+
' encryptedData: VaultEncryptedData,',
|
|
1269
|
+
' aad?: string,',
|
|
1270
|
+
' ): Promise<string> {',
|
|
1271
|
+
' const dataKey = await this.decryptDataKey({',
|
|
1272
|
+
' keys: encryptedData.encryptedKeys,',
|
|
1273
|
+
' });',
|
|
1274
|
+
' const encodedAad = aad ? new TextEncoder().encode(aad) : undefined;',
|
|
1275
|
+
' const decrypted = await this.workos.getCryptoProvider().decrypt(',
|
|
1276
|
+
' base64ToUint8Array(encryptedData.ciphertext),',
|
|
1277
|
+
' base64ToUint8Array(dataKey.key),',
|
|
1278
|
+
' base64ToUint8Array(encryptedData.iv),',
|
|
1279
|
+
' base64ToUint8Array(encryptedData.tag),',
|
|
1280
|
+
' encodedAad,',
|
|
1281
|
+
' );',
|
|
1282
|
+
'',
|
|
1283
|
+
' return new TextDecoder().decode(decrypted);',
|
|
1284
|
+
' }',
|
|
1285
|
+
];
|
|
1286
|
+
}
|
|
1287
|
+
|
|
924
1288
|
function renderMethod(
|
|
925
1289
|
op: Operation,
|
|
926
1290
|
plan: OperationPlan,
|
|
@@ -948,9 +1312,12 @@ function renderMethod(
|
|
|
948
1312
|
// otherwise compute from what the render path will actually include.
|
|
949
1313
|
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
950
1314
|
const baselineClassMethod = baselineMethodFor(service, method, ctx);
|
|
1315
|
+
const optionInfo = optionsObjectInfo(service, method, op, plan, ctx, baselineClassMethod, resolvedOp);
|
|
951
1316
|
const overlayMethod = ctx.overlayLookup?.methodByOperation?.get(httpKey) ?? baselineClassMethod;
|
|
952
1317
|
let validParamNames: Set<string> | null = null;
|
|
953
|
-
if (
|
|
1318
|
+
if (optionInfo) {
|
|
1319
|
+
validParamNames = new Set(['options']);
|
|
1320
|
+
} else if (overlayMethod) {
|
|
954
1321
|
validParamNames = new Set(overlayMethod.params.map((p) => p.name));
|
|
955
1322
|
} else {
|
|
956
1323
|
// Compute actual params based on render path to avoid documenting params
|
|
@@ -1148,7 +1515,9 @@ function renderMethod(
|
|
|
1148
1515
|
const unwrapped = unwrapListModel(pModel, modelMap);
|
|
1149
1516
|
if (unwrapped) itemRawName = unwrapped.name;
|
|
1150
1517
|
}
|
|
1151
|
-
const
|
|
1518
|
+
const baselineItemType =
|
|
1519
|
+
autoPaginatableItemType(baselineClassMethod?.returnType) ?? autoPaginatableItemType(overlayMethod?.returnType);
|
|
1520
|
+
const itemTypeName = preferredBaselineTypeName(ctx, baselineItemType) ?? resolveInterfaceName(itemRawName, ctx);
|
|
1152
1521
|
docParts.push(`@returns {Promise<AutoPaginatable<${itemTypeName}>>}`);
|
|
1153
1522
|
} else if (responseModel) {
|
|
1154
1523
|
const returnTypeDoc = plan.isArrayResponse ? `${responseModel}[]` : responseModel;
|
|
@@ -1185,7 +1554,20 @@ function renderMethod(
|
|
|
1185
1554
|
}
|
|
1186
1555
|
}
|
|
1187
1556
|
|
|
1188
|
-
if (
|
|
1557
|
+
if (
|
|
1558
|
+
renderOptionsObjectMethod(
|
|
1559
|
+
lines,
|
|
1560
|
+
op,
|
|
1561
|
+
plan,
|
|
1562
|
+
method,
|
|
1563
|
+
service,
|
|
1564
|
+
ctx,
|
|
1565
|
+
modelMap,
|
|
1566
|
+
specEnumNames,
|
|
1567
|
+
baselineClassMethod,
|
|
1568
|
+
resolvedOp,
|
|
1569
|
+
)
|
|
1570
|
+
) {
|
|
1189
1571
|
return lines;
|
|
1190
1572
|
}
|
|
1191
1573
|
|
|
@@ -1257,13 +1639,25 @@ function renderOptionsObjectMethod(
|
|
|
1257
1639
|
modelMap: Map<string, Model>,
|
|
1258
1640
|
specEnumNames: Set<string> | undefined,
|
|
1259
1641
|
baselineMethod: BaselineMethod | undefined,
|
|
1642
|
+
resolvedOp: ResolvedOperation | undefined,
|
|
1260
1643
|
): boolean {
|
|
1261
|
-
const optionParam =
|
|
1644
|
+
const optionParam = optionsObjectInfo(service, method, op, plan, ctx, baselineMethod, resolvedOp);
|
|
1262
1645
|
if (!optionParam) return false;
|
|
1263
1646
|
|
|
1264
1647
|
const responseModel = plan.responseModelName ? resolveInterfaceName(plan.responseModelName, ctx) : null;
|
|
1265
1648
|
const pathBindings = buildOptionsObjectPathBindings(op, optionParam.type, ctx);
|
|
1266
1649
|
const pathStr = buildPathStr(op, buildOptionsObjectPathParamMap(op, optionParam.type, ctx));
|
|
1650
|
+
const override = operationOverrideFor(ctx, op);
|
|
1651
|
+
const bodyFieldMap = override?.bodyFieldMap;
|
|
1652
|
+
const visibleQueryParams = visibleQueryParamsForOptions(op, plan, resolvedOp);
|
|
1653
|
+
const queryBindings = visibleQueryParams.map((param) => fieldName(param.name));
|
|
1654
|
+
const hasQuery =
|
|
1655
|
+
visibleQueryParams.length > 0 ||
|
|
1656
|
+
Object.keys(getOpDefaults(resolvedOp)).length > 0 ||
|
|
1657
|
+
getOpInferFromClient(resolvedOp).length > 0;
|
|
1658
|
+
const queryOptionsArg = hasQuery
|
|
1659
|
+
? `, { query: ${renderQueryExprWithOptions(visibleQueryParams, optionParam.optional, resolvedOp)} }`
|
|
1660
|
+
: '';
|
|
1267
1661
|
|
|
1268
1662
|
if (plan.isPaginated && op.pagination && op.httpMethod === 'get') {
|
|
1269
1663
|
let itemRawName = op.pagination.itemType.kind === 'model' ? op.pagination.itemType.name : null;
|
|
@@ -1281,14 +1675,19 @@ function renderOptionsObjectMethod(
|
|
|
1281
1675
|
const wireType = wireInterfaceName(itemType);
|
|
1282
1676
|
const returnType =
|
|
1283
1677
|
preferredBaselineReturnType(ctx, baselineMethod?.returnType) ?? `Promise<AutoPaginatable<${itemType}>>`;
|
|
1284
|
-
|
|
1285
|
-
|
|
1678
|
+
const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
|
|
1679
|
+
const needsWireSerializer = extraParams.some((p) => fieldName(p.name) !== wireFieldName(p.name));
|
|
1680
|
+
const listOptionsExpr = needsWireSerializer
|
|
1681
|
+
? `options ? serialize${optionParam.type}(options) : undefined`
|
|
1682
|
+
: 'paginationOptions';
|
|
1683
|
+
lines.push(` async ${method}(${renderOptionsParam(optionParam)}): ${returnType} {`);
|
|
1684
|
+
renderOptionsObjectDestructure(lines, pathBindings, needsWireSerializer ? undefined : 'paginationOptions');
|
|
1286
1685
|
lines.push(` return new AutoPaginatable(`);
|
|
1287
1686
|
lines.push(` await fetchAndDeserialize<${wireType}, ${itemType}>(`);
|
|
1288
1687
|
lines.push(` this.workos,`);
|
|
1289
1688
|
lines.push(` ${pathStr},`);
|
|
1290
1689
|
lines.push(` deserialize${itemType},`);
|
|
1291
|
-
lines.push(`
|
|
1690
|
+
lines.push(` ${listOptionsExpr},`);
|
|
1292
1691
|
lines.push(` ),`);
|
|
1293
1692
|
lines.push(` (params) =>`);
|
|
1294
1693
|
lines.push(` fetchAndDeserialize<${wireType}, ${itemType}>(`);
|
|
@@ -1297,16 +1696,16 @@ function renderOptionsObjectMethod(
|
|
|
1297
1696
|
lines.push(` deserialize${itemType},`);
|
|
1298
1697
|
lines.push(` params,`);
|
|
1299
1698
|
lines.push(` ),`);
|
|
1300
|
-
lines.push(`
|
|
1699
|
+
lines.push(` ${listOptionsExpr},`);
|
|
1301
1700
|
lines.push(` );`);
|
|
1302
1701
|
lines.push(' }');
|
|
1303
1702
|
return true;
|
|
1304
1703
|
}
|
|
1305
1704
|
|
|
1306
1705
|
if (plan.isDelete && !plan.hasBody) {
|
|
1307
|
-
lines.push(` async ${method}(
|
|
1706
|
+
lines.push(` async ${method}(${renderOptionsParam(optionParam)}): Promise<void> {`);
|
|
1308
1707
|
renderOptionsObjectDestructure(lines, pathBindings);
|
|
1309
|
-
lines.push(` await this.workos.delete(${pathStr});`);
|
|
1708
|
+
lines.push(` await this.workos.delete(${pathStr}${queryOptionsArg});`);
|
|
1310
1709
|
lines.push(' }');
|
|
1311
1710
|
return true;
|
|
1312
1711
|
}
|
|
@@ -1333,49 +1732,71 @@ function renderOptionsObjectMethod(
|
|
|
1333
1732
|
}
|
|
1334
1733
|
|
|
1335
1734
|
if (plan.isDelete) {
|
|
1336
|
-
lines.push(` async ${method}(
|
|
1337
|
-
renderOptionsObjectDestructure(lines, pathBindings, 'payload');
|
|
1338
|
-
|
|
1735
|
+
lines.push(` async ${method}(${renderOptionsParam(optionParam)}): Promise<void> {`);
|
|
1736
|
+
renderOptionsObjectDestructure(lines, [...pathBindings, ...queryBindings], 'payload');
|
|
1737
|
+
const bodyParam = renderOptionsObjectBodyFieldMap(lines, bodyFieldMap);
|
|
1738
|
+
bodyExpr = bodyArgExprWithParam(bodyExpr, bodyParam);
|
|
1739
|
+
lines.push(` await this.workos.deleteWithBody<${entityType}>(${pathStr}, ${bodyExpr}${queryOptionsArg});`);
|
|
1339
1740
|
lines.push(' }');
|
|
1340
1741
|
return true;
|
|
1341
1742
|
}
|
|
1342
1743
|
|
|
1343
1744
|
if (!responseModel) {
|
|
1344
|
-
lines.push(` async ${method}(
|
|
1345
|
-
renderOptionsObjectDestructure(lines, pathBindings, 'payload');
|
|
1346
|
-
|
|
1745
|
+
lines.push(` async ${method}(${renderOptionsParam(optionParam)}): Promise<void> {`);
|
|
1746
|
+
renderOptionsObjectDestructure(lines, [...pathBindings, ...queryBindings], 'payload');
|
|
1747
|
+
const bodyParam = renderOptionsObjectBodyFieldMap(lines, bodyFieldMap);
|
|
1748
|
+
bodyExpr = bodyArgExprWithParam(bodyExpr, bodyParam);
|
|
1749
|
+
lines.push(
|
|
1750
|
+
` await this.workos.${op.httpMethod}<void, ${entityType}>(${pathStr}, ${bodyExpr}${queryOptionsArg});`,
|
|
1751
|
+
);
|
|
1347
1752
|
lines.push(' }');
|
|
1348
1753
|
return true;
|
|
1349
1754
|
}
|
|
1350
1755
|
|
|
1351
1756
|
const returnType = plan.isArrayResponse ? `${responseModel}[]` : responseModel;
|
|
1757
|
+
const methodReturnType = override?.returnType ?? `Promise<${returnType}>`;
|
|
1352
1758
|
const wireType = plan.isArrayResponse ? `${wireInterfaceName(responseModel)}[]` : wireInterfaceName(responseModel);
|
|
1353
1759
|
const returnExpr = plan.isArrayResponse
|
|
1354
1760
|
? `data.map(deserialize${responseModel})`
|
|
1355
1761
|
: `deserialize${responseModel}(data)`;
|
|
1762
|
+
const finalReturnExpr = override?.returnDataProperty ? `${returnExpr}.${override.returnDataProperty}` : returnExpr;
|
|
1356
1763
|
|
|
1357
|
-
lines.push(` async ${method}(
|
|
1358
|
-
renderOptionsObjectDestructure(lines, pathBindings, 'payload');
|
|
1764
|
+
lines.push(` async ${method}(${renderOptionsParam(optionParam)}): ${methodReturnType} {`);
|
|
1765
|
+
renderOptionsObjectDestructure(lines, [...pathBindings, ...queryBindings], 'payload');
|
|
1766
|
+
const bodyParam = renderOptionsObjectBodyFieldMap(lines, bodyFieldMap);
|
|
1767
|
+
bodyExpr = bodyArgExprWithParam(bodyExpr, bodyParam);
|
|
1359
1768
|
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}, ${entityType}>(`);
|
|
1360
1769
|
lines.push(` ${pathStr},`);
|
|
1361
|
-
lines.push(` ${bodyExpr},`);
|
|
1770
|
+
lines.push(` ${bodyExpr}${queryOptionsArg},`);
|
|
1362
1771
|
lines.push(' );');
|
|
1363
|
-
|
|
1772
|
+
if (override?.returnExpression) {
|
|
1773
|
+
lines.push(` const result = ${returnExpr};`);
|
|
1774
|
+
lines.push(` return ${override.returnExpression};`);
|
|
1775
|
+
} else {
|
|
1776
|
+
lines.push(` return ${finalReturnExpr};`);
|
|
1777
|
+
}
|
|
1364
1778
|
lines.push(' }');
|
|
1365
1779
|
return true;
|
|
1366
1780
|
}
|
|
1367
1781
|
|
|
1368
1782
|
if (responseModel) {
|
|
1369
1783
|
const returnType = plan.isArrayResponse ? `${responseModel}[]` : responseModel;
|
|
1784
|
+
const methodReturnType = override?.returnType ?? `Promise<${returnType}>`;
|
|
1370
1785
|
const wireType = plan.isArrayResponse ? `${wireInterfaceName(responseModel)}[]` : wireInterfaceName(responseModel);
|
|
1371
1786
|
const returnExpr = plan.isArrayResponse
|
|
1372
1787
|
? `data.map(deserialize${responseModel})`
|
|
1373
1788
|
: `deserialize${responseModel}(data)`;
|
|
1789
|
+
const finalReturnExpr = override?.returnDataProperty ? `${returnExpr}.${override.returnDataProperty}` : returnExpr;
|
|
1374
1790
|
|
|
1375
|
-
lines.push(` async ${method}(
|
|
1791
|
+
lines.push(` async ${method}(${renderOptionsParam(optionParam)}): ${methodReturnType} {`);
|
|
1376
1792
|
renderOptionsObjectDestructure(lines, pathBindings);
|
|
1377
|
-
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr});`);
|
|
1378
|
-
|
|
1793
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr}${queryOptionsArg});`);
|
|
1794
|
+
if (override?.returnExpression) {
|
|
1795
|
+
lines.push(` const result = ${returnExpr};`);
|
|
1796
|
+
lines.push(` return ${override.returnExpression};`);
|
|
1797
|
+
} else {
|
|
1798
|
+
lines.push(` return ${finalReturnExpr};`);
|
|
1799
|
+
}
|
|
1379
1800
|
lines.push(' }');
|
|
1380
1801
|
return true;
|
|
1381
1802
|
}
|
|
@@ -1393,6 +1814,23 @@ function renderOptionsObjectDestructure(lines: string[], pathBindings: string[],
|
|
|
1393
1814
|
}
|
|
1394
1815
|
}
|
|
1395
1816
|
|
|
1817
|
+
function renderOptionsObjectBodyFieldMap(lines: string[], bodyFieldMap: Record<string, string> | undefined): string {
|
|
1818
|
+
const entries = Object.entries(bodyFieldMap ?? {});
|
|
1819
|
+
if (entries.length === 0) return 'payload';
|
|
1820
|
+
|
|
1821
|
+
lines.push(' const requestPayload = {');
|
|
1822
|
+
lines.push(' ...payload,');
|
|
1823
|
+
for (const [source, target] of entries) {
|
|
1824
|
+
lines.push(` ${target}: payload.${source},`);
|
|
1825
|
+
}
|
|
1826
|
+
lines.push(' };');
|
|
1827
|
+
return 'requestPayload';
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
function bodyArgExprWithParam(bodyExpr: string, paramName: string): string {
|
|
1831
|
+
return paramName === 'payload' ? bodyExpr : bodyExpr.replace(/\bpayload\b/g, paramName);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1396
1834
|
function buildOptionsObjectPathBindings(op: Operation, optionType: string, ctx: EmitterContext): string[] {
|
|
1397
1835
|
// Return resolved SDK field names directly — the URL template uses these
|
|
1398
1836
|
// names too (via the param-name map threaded into buildNodePathExpression),
|
|
@@ -1464,7 +1902,7 @@ function renderPaginatedMethod(
|
|
|
1464
1902
|
lines.push(` deserialize${itemType},`);
|
|
1465
1903
|
lines.push(` params,`);
|
|
1466
1904
|
lines.push(` ),`);
|
|
1467
|
-
lines.push(`
|
|
1905
|
+
lines.push(` ${serializeCall},`);
|
|
1468
1906
|
lines.push(` );`);
|
|
1469
1907
|
lines.push(' }');
|
|
1470
1908
|
}
|
|
@@ -1862,6 +2300,32 @@ function renderQueryExpr(queryParams: { name: string; required: boolean }[]): st
|
|
|
1862
2300
|
return `options ? { ${parts.join(', ')} } : undefined`;
|
|
1863
2301
|
}
|
|
1864
2302
|
|
|
2303
|
+
function renderQueryExprWithOptions(
|
|
2304
|
+
queryParams: { name: string; required: boolean }[],
|
|
2305
|
+
optional: boolean,
|
|
2306
|
+
resolvedOp?: ResolvedOperation,
|
|
2307
|
+
): string {
|
|
2308
|
+
const parts: string[] = [];
|
|
2309
|
+
for (const param of queryParams) {
|
|
2310
|
+
const camel = fieldName(param.name);
|
|
2311
|
+
const snake = wireFieldName(param.name);
|
|
2312
|
+
const access = optional ? `options?.${camel}` : `options.${camel}`;
|
|
2313
|
+
if (param.required) {
|
|
2314
|
+
parts.push(`${snake}: ${access}`);
|
|
2315
|
+
} else {
|
|
2316
|
+
parts.push(`...(${access} !== undefined && { ${snake}: ${access} })`);
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
for (const [key, value] of Object.entries(getOpDefaults(resolvedOp))) {
|
|
2320
|
+
parts.push(`${key}: ${tsLiteral(value)}`);
|
|
2321
|
+
}
|
|
2322
|
+
for (const field of getOpInferFromClient(resolvedOp)) {
|
|
2323
|
+
parts.push(`${field}: ${clientFieldExpression(field)}`);
|
|
2324
|
+
}
|
|
2325
|
+
if (parts.length === 0) return optional ? 'options' : '{}';
|
|
2326
|
+
return `{ ${parts.join(', ')} }`;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
1865
2329
|
function buildPathStr(op: Operation, paramNameMap?: Map<string, string>): string {
|
|
1866
2330
|
return buildNodePathExpression(op.path, paramNameMap);
|
|
1867
2331
|
}
|