@workos/oagen-emitters 0.3.0 → 0.5.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/.github/workflows/ci.yml +1 -1
- package/.github/workflows/lint.yml +1 -1
- package/.github/workflows/release-please.yml +2 -2
- package/.github/workflows/release.yml +1 -1
- package/.husky/pre-push +11 -0
- package/.node-version +1 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +15 -0
- package/README.md +35 -224
- package/dist/index.d.mts +12 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -12737
- package/dist/plugin-BSop9f9z.mjs +21471 -0
- package/dist/plugin-BSop9f9z.mjs.map +1 -0
- package/dist/plugin.d.mts +7 -0
- package/dist/plugin.d.mts.map +1 -0
- package/dist/plugin.mjs +2 -0
- package/docs/sdk-architecture/dotnet.md +336 -0
- package/oagen.config.ts +5 -343
- package/package.json +10 -34
- package/smoke/sdk-dotnet.ts +45 -12
- package/src/dotnet/client.ts +89 -0
- package/src/dotnet/enums.ts +323 -0
- package/src/dotnet/fixtures.ts +236 -0
- package/src/dotnet/index.ts +248 -0
- package/src/dotnet/manifest.ts +36 -0
- package/src/dotnet/models.ts +320 -0
- package/src/dotnet/naming.ts +368 -0
- package/src/dotnet/resources.ts +943 -0
- package/src/dotnet/tests.ts +713 -0
- package/src/dotnet/type-map.ts +228 -0
- package/src/dotnet/wrappers.ts +197 -0
- package/src/go/client.ts +35 -3
- package/src/go/enums.ts +4 -0
- package/src/go/index.ts +15 -7
- package/src/go/models.ts +6 -1
- package/src/go/naming.ts +5 -17
- package/src/go/resources.ts +534 -73
- package/src/go/tests.ts +39 -3
- package/src/go/type-map.ts +8 -3
- package/src/go/wrappers.ts +79 -21
- package/src/index.ts +15 -0
- package/src/kotlin/client.ts +58 -0
- package/src/kotlin/enums.ts +189 -0
- package/src/kotlin/index.ts +92 -0
- package/src/kotlin/manifest.ts +55 -0
- package/src/kotlin/models.ts +486 -0
- package/src/kotlin/naming.ts +229 -0
- package/src/kotlin/overrides.ts +25 -0
- package/src/kotlin/resources.ts +998 -0
- package/src/kotlin/tests.ts +1133 -0
- package/src/kotlin/type-map.ts +123 -0
- package/src/kotlin/wrappers.ts +168 -0
- package/src/node/client.ts +84 -7
- package/src/node/field-plan.ts +12 -14
- package/src/node/fixtures.ts +39 -3
- package/src/node/index.ts +1 -0
- package/src/node/models.ts +281 -37
- package/src/node/resources.ts +319 -95
- package/src/node/tests.ts +108 -29
- package/src/node/type-map.ts +1 -31
- package/src/node/utils.ts +96 -6
- package/src/node/wrappers.ts +31 -1
- package/src/php/client.ts +11 -3
- package/src/php/models.ts +0 -33
- package/src/php/naming.ts +2 -21
- package/src/php/resources.ts +275 -19
- package/src/php/tests.ts +118 -18
- package/src/php/type-map.ts +16 -2
- package/src/php/wrappers.ts +7 -2
- package/src/plugin.ts +50 -0
- package/src/python/client.ts +50 -32
- package/src/python/enums.ts +35 -10
- package/src/python/index.ts +35 -27
- package/src/python/models.ts +139 -2
- package/src/python/naming.ts +2 -22
- package/src/python/resources.ts +234 -17
- package/src/python/tests.ts +260 -16
- package/src/python/type-map.ts +16 -2
- package/src/ruby/client.ts +238 -0
- package/src/ruby/enums.ts +149 -0
- package/src/ruby/index.ts +93 -0
- package/src/ruby/manifest.ts +35 -0
- package/src/ruby/models.ts +360 -0
- package/src/ruby/naming.ts +187 -0
- package/src/ruby/rbi.ts +313 -0
- package/src/ruby/resources.ts +799 -0
- package/src/ruby/tests.ts +459 -0
- package/src/ruby/type-map.ts +97 -0
- package/src/ruby/wrappers.ts +161 -0
- package/src/shared/model-utils.ts +357 -16
- package/src/shared/naming-utils.ts +83 -0
- package/src/shared/non-spec-services.ts +13 -0
- package/src/shared/resolved-ops.ts +75 -1
- package/src/shared/wrapper-utils.ts +12 -1
- package/test/dotnet/client.test.ts +121 -0
- package/test/dotnet/enums.test.ts +193 -0
- package/test/dotnet/errors.test.ts +9 -0
- package/test/dotnet/manifest.test.ts +82 -0
- package/test/dotnet/models.test.ts +258 -0
- package/test/dotnet/resources.test.ts +387 -0
- package/test/dotnet/tests.test.ts +202 -0
- package/test/entrypoint.test.ts +89 -0
- package/test/go/client.test.ts +6 -6
- package/test/go/resources.test.ts +156 -7
- package/test/kotlin/models.test.ts +135 -0
- package/test/kotlin/resources.test.ts +210 -0
- package/test/kotlin/tests.test.ts +176 -0
- package/test/node/client.test.ts +74 -0
- package/test/node/models.test.ts +134 -1
- package/test/node/resources.test.ts +343 -34
- package/test/node/utils.test.ts +140 -0
- package/test/php/client.test.ts +2 -1
- package/test/php/models.test.ts +5 -4
- package/test/php/resources.test.ts +103 -0
- package/test/php/tests.test.ts +67 -0
- package/test/plugin.test.ts +50 -0
- package/test/python/client.test.ts +56 -0
- package/test/python/models.test.ts +99 -0
- package/test/python/resources.test.ts +294 -0
- package/test/python/tests.test.ts +91 -0
- package/test/ruby/client.test.ts +81 -0
- package/test/ruby/resources.test.ts +386 -0
- package/test/shared/resolved-ops.test.ts +122 -0
- package/tsdown.config.ts +1 -1
- package/dist/index.mjs.map +0 -1
- package/scripts/generate-php.js +0 -13
- package/scripts/git-push-with-published-oagen.sh +0 -21
package/src/node/resources.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
getOpInferFromClient,
|
|
41
41
|
} from '../shared/resolved-ops.js';
|
|
42
42
|
import { generateWrapperMethods, collectWrapperResponseModels } from './wrappers.js';
|
|
43
|
+
import { resolveWrapperParams } from '../shared/wrapper-utils.js';
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* Check whether the baseline (hand-written) class has a constructor compatible
|
|
@@ -192,6 +193,62 @@ function deduplicateMethodNames(
|
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Emit one interface file per paginated list operation that has extension
|
|
198
|
+
* query params. Placing the options interface under `interfaces/` lets the
|
|
199
|
+
* per-service barrel pick it up via `export * from './interfaces'`, which
|
|
200
|
+
* is what the root `src/index.ts` re-exports. When the interface was
|
|
201
|
+
* declared inline in the resource file, it was unreachable from the barrel
|
|
202
|
+
* and callers couldn't import the type by name from the package root.
|
|
203
|
+
*/
|
|
204
|
+
function generatePaginatedOptionsInterfaces(
|
|
205
|
+
service: Service,
|
|
206
|
+
ctx: EmitterContext,
|
|
207
|
+
specEnumNames: Set<string>,
|
|
208
|
+
): GeneratedFile[] {
|
|
209
|
+
const files: GeneratedFile[] = [];
|
|
210
|
+
const resolvedName = resolveResourceClassName(service, ctx);
|
|
211
|
+
const serviceDir = resolveServiceDir(resolvedName);
|
|
212
|
+
|
|
213
|
+
const plans = service.operations.map((op) => ({
|
|
214
|
+
op,
|
|
215
|
+
plan: planOperation(op),
|
|
216
|
+
method: resolveMethodName(op, service, ctx),
|
|
217
|
+
}));
|
|
218
|
+
|
|
219
|
+
for (const { op, plan, method } of plans) {
|
|
220
|
+
if (!plan.isPaginated) continue;
|
|
221
|
+
const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
|
|
222
|
+
if (extraParams.length === 0) continue;
|
|
223
|
+
|
|
224
|
+
const optionsName = paginatedOptionsName(method, resolvedName);
|
|
225
|
+
const filePath = `src/${serviceDir}/interfaces/${fileName(optionsName)}.interface.ts`;
|
|
226
|
+
|
|
227
|
+
const lines: string[] = [];
|
|
228
|
+
lines.push("import type { PaginationOptions } from '../../common/interfaces/pagination-options.interface';");
|
|
229
|
+
lines.push('');
|
|
230
|
+
lines.push(`export interface ${optionsName} extends PaginationOptions {`);
|
|
231
|
+
for (const param of extraParams) {
|
|
232
|
+
const opt = !param.required ? '?' : '';
|
|
233
|
+
if (param.description || param.deprecated) {
|
|
234
|
+
const parts: string[] = [];
|
|
235
|
+
if (param.description) parts.push(param.description);
|
|
236
|
+
if (param.deprecated) parts.push('@deprecated');
|
|
237
|
+
lines.push(...docComment(parts.join('\n'), 2));
|
|
238
|
+
}
|
|
239
|
+
lines.push(` ${fieldName(param.name)}${opt}: ${mapParamType(param.type, specEnumNames)};`);
|
|
240
|
+
}
|
|
241
|
+
lines.push('}');
|
|
242
|
+
|
|
243
|
+
files.push({
|
|
244
|
+
path: filePath,
|
|
245
|
+
content: lines.join('\n'),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return files;
|
|
250
|
+
}
|
|
251
|
+
|
|
195
252
|
export function generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[] {
|
|
196
253
|
if (services.length === 0) return [];
|
|
197
254
|
const files: GeneratedFile[] = [];
|
|
@@ -202,19 +259,19 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
202
259
|
const mergedServices: Service[] =
|
|
203
260
|
mountGroups.size > 0 ? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations })) : services;
|
|
204
261
|
|
|
262
|
+
const topLevelEnumNames = new Set(ctx.spec.enums.map((e) => e.name));
|
|
263
|
+
|
|
205
264
|
for (const service of mergedServices) {
|
|
206
265
|
if (isServiceCoveredByExisting(service, ctx)) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const file = generateResourceClass(service, ctx);
|
|
210
|
-
// When the baseline class is missing methods for some operations,
|
|
211
|
-
// remove skipIfExists so the merger adds the new methods.
|
|
212
|
-
if (hasMethodsAbsentFromBaseline(service, ctx)) {
|
|
213
|
-
delete file.skipIfExists;
|
|
214
|
-
// Suppress auto-generated header — the file is a merge target
|
|
215
|
-
// containing hand-written code, not a fully generated file.
|
|
216
|
-
file.headerPlacement = 'skip';
|
|
266
|
+
if (!hasMethodsAbsentFromBaseline(service, ctx)) {
|
|
267
|
+
continue; // Fully covered, no new methods -- skip entirely
|
|
217
268
|
}
|
|
269
|
+
// Partially covered -- has new methods that need to be added.
|
|
270
|
+
const file = generateResourceClass(service, ctx);
|
|
271
|
+
delete file.skipIfExists;
|
|
272
|
+
// Suppress auto-generated header — the file is a merge target
|
|
273
|
+
// containing hand-written code, not a fully generated file.
|
|
274
|
+
file.headerPlacement = 'skip';
|
|
218
275
|
files.push(file);
|
|
219
276
|
continue;
|
|
220
277
|
}
|
|
@@ -233,10 +290,25 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
233
290
|
file.headerPlacement = 'skip';
|
|
234
291
|
files.push(file);
|
|
235
292
|
} else {
|
|
236
|
-
|
|
293
|
+
// Purely oagen-managed: no baseline class exists, so the file is owned
|
|
294
|
+
// end-to-end by the emitter. Remove skipIfExists so regeneration always
|
|
295
|
+
// overwrites — emitter improvements (serializer dispatch, JSDoc, etc.)
|
|
296
|
+
// must propagate without manual intervention.
|
|
297
|
+
const file = generateResourceClass(service, ctx);
|
|
298
|
+
delete file.skipIfExists;
|
|
299
|
+
files.push(file);
|
|
237
300
|
}
|
|
238
301
|
}
|
|
239
302
|
|
|
303
|
+
// Emit paginated list options interfaces AFTER the resource classes so
|
|
304
|
+
// tests and manifest ordering that index `files[0]` as the class stay
|
|
305
|
+
// stable. Placing them under `interfaces/` lets the per-service barrel
|
|
306
|
+
// pick them up automatically.
|
|
307
|
+
for (const service of mergedServices) {
|
|
308
|
+
if (isServiceCoveredByExisting(service, ctx) && !hasMethodsAbsentFromBaseline(service, ctx)) continue;
|
|
309
|
+
files.push(...generatePaginatedOptionsInterfaces(service, ctx, topLevelEnumNames));
|
|
310
|
+
}
|
|
311
|
+
|
|
240
312
|
return files;
|
|
241
313
|
}
|
|
242
314
|
|
|
@@ -311,11 +383,19 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
311
383
|
const paramEnums = new Set<string>();
|
|
312
384
|
const paramModels = new Set<string>();
|
|
313
385
|
for (const { op, plan, method } of plans) {
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
386
|
+
// Always collect param type refs for enums — inline options interfaces
|
|
387
|
+
// are generated for all methods (including baseline ones), so their
|
|
388
|
+
// type dependencies must always be imported.
|
|
389
|
+
const queryParams = plan.isPaginated
|
|
390
|
+
? op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name))
|
|
391
|
+
: op.queryParams;
|
|
392
|
+
for (const param of [...queryParams, ...op.pathParams]) {
|
|
393
|
+
collectParamTypeRefs(param.type, paramEnums, paramModels);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Skip response/request model imports for methods that already exist in
|
|
397
|
+
// the baseline class. The merger keeps baseline method bodies, so their
|
|
398
|
+
// imports are already present in the existing file.
|
|
319
399
|
if (baselineMethodSet.has(method)) continue;
|
|
320
400
|
|
|
321
401
|
if (plan.isPaginated && op.pagination?.itemType.kind === 'model') {
|
|
@@ -354,19 +434,13 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
354
434
|
}
|
|
355
435
|
}
|
|
356
436
|
}
|
|
357
|
-
// Collect types referenced in query and path parameters.
|
|
358
|
-
// For paginated operations, skip standard pagination params (limit, before, after, order)
|
|
359
|
-
// since they're handled by PaginationOptions and don't need explicit imports.
|
|
360
|
-
const queryParams = plan.isPaginated
|
|
361
|
-
? op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name))
|
|
362
|
-
: op.queryParams;
|
|
363
|
-
for (const param of [...queryParams, ...op.pathParams]) {
|
|
364
|
-
collectParamTypeRefs(param.type, paramEnums, paramModels);
|
|
365
|
-
}
|
|
366
437
|
}
|
|
367
438
|
|
|
368
439
|
// Collect response models from union split wrappers so their types and
|
|
369
440
|
// deserializers are imported alongside the primary operation models.
|
|
441
|
+
// Also collect models referenced in wrapper param signatures (e.g.,
|
|
442
|
+
// `redirect_uris: RedirectUriInput[]`) — otherwise the wrapper emits a
|
|
443
|
+
// reference to a type it never imported.
|
|
370
444
|
const resolvedLookup = buildResolvedLookup(ctx);
|
|
371
445
|
for (const { op, method } of plans) {
|
|
372
446
|
if (baselineMethodSet.has(method)) continue;
|
|
@@ -375,6 +449,11 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
375
449
|
for (const name of collectWrapperResponseModels(resolved)) {
|
|
376
450
|
responseModels.add(name);
|
|
377
451
|
}
|
|
452
|
+
for (const wrapper of resolved.wrappers ?? []) {
|
|
453
|
+
for (const { field } of resolveWrapperParams(wrapper, ctx)) {
|
|
454
|
+
if (field) collectParamTypeRefs(field.type, paramEnums, paramModels);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
378
457
|
}
|
|
379
458
|
}
|
|
380
459
|
|
|
@@ -386,8 +465,19 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
386
465
|
lines.push("import type { WorkOS } from '../workos';");
|
|
387
466
|
if (hasPaginated) {
|
|
388
467
|
lines.push("import type { PaginationOptions } from '../common/interfaces/pagination-options.interface';");
|
|
389
|
-
lines.push("import
|
|
390
|
-
lines.push("import {
|
|
468
|
+
lines.push("import { AutoPaginatable } from '../common/utils/pagination';");
|
|
469
|
+
lines.push("import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize';");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Paginated list options live in their own interface files so they're
|
|
473
|
+
// picked up by the per-service barrel (and flow through to the root
|
|
474
|
+
// package barrel). Import them here rather than declaring inline.
|
|
475
|
+
for (const { op, plan, method } of plans) {
|
|
476
|
+
if (!plan.isPaginated) continue;
|
|
477
|
+
const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
|
|
478
|
+
if (extraParams.length === 0) continue;
|
|
479
|
+
const optionsName = paginatedOptionsName(method, resolvedName);
|
|
480
|
+
lines.push(`import type { ${optionsName} } from './interfaces/${fileName(optionsName)}.interface';`);
|
|
391
481
|
}
|
|
392
482
|
|
|
393
483
|
// Check if any operation needs PostOptions (idempotent POST or custom encoding)
|
|
@@ -493,32 +583,43 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
493
583
|
|
|
494
584
|
lines.push('');
|
|
495
585
|
|
|
496
|
-
//
|
|
497
|
-
//
|
|
586
|
+
// Per-operation helpers (wire-format option serializers etc.) emitted
|
|
587
|
+
// alongside the resource class. The options interfaces themselves live
|
|
588
|
+
// in separate files under `interfaces/` so the per-service barrel can
|
|
589
|
+
// re-export them; see the earlier import block at the top of the file.
|
|
498
590
|
for (const { op, plan, method } of plans) {
|
|
499
591
|
if (plan.isPaginated) {
|
|
500
592
|
const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
|
|
501
593
|
if (extraParams.length > 0) {
|
|
502
594
|
const optionsName = paginatedOptionsName(method, resolvedName);
|
|
503
|
-
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
595
|
+
|
|
596
|
+
// When any extension param has a camelCase domain name that differs
|
|
597
|
+
// from its snake_case wire name, emit a serializer that translates
|
|
598
|
+
// the user-facing options into the wire query shape. Without this,
|
|
599
|
+
// the query string uses camelCase keys (e.g. `organizationId=...`)
|
|
600
|
+
// that the API silently ignores — the filter becomes a no-op.
|
|
601
|
+
const needsWireSerializer = extraParams.some((p) => fieldName(p.name) !== wireFieldName(p.name));
|
|
602
|
+
if (needsWireSerializer) {
|
|
603
|
+
const serializerName = `serialize${optionsName}`;
|
|
604
|
+
lines.push(`const ${serializerName} = (options: ${optionsName}): PaginationOptions => {`);
|
|
605
|
+
// Pagination fields pass through unchanged (limit/before/after/order
|
|
606
|
+
// share spelling in both cases). Spread first so that wire-named
|
|
607
|
+
// extension fields land on top and the camelCase keys don't also
|
|
608
|
+
// leak into the query string.
|
|
609
|
+
lines.push(' const wire: Record<string, unknown> = {');
|
|
610
|
+
for (const p of PAGINATION_PARAM_NAMES) {
|
|
611
|
+
lines.push(` ${p}: options.${p},`);
|
|
517
612
|
}
|
|
518
|
-
lines.push(
|
|
613
|
+
lines.push(' };');
|
|
614
|
+
for (const param of extraParams) {
|
|
615
|
+
const camel = fieldName(param.name);
|
|
616
|
+
const snake = wireFieldName(param.name);
|
|
617
|
+
lines.push(` if (options.${camel} !== undefined) wire.${snake} = options.${camel};`);
|
|
618
|
+
}
|
|
619
|
+
lines.push(' return wire as PaginationOptions;');
|
|
620
|
+
lines.push('};');
|
|
621
|
+
lines.push('');
|
|
519
622
|
}
|
|
520
|
-
lines.push('}');
|
|
521
|
-
lines.push('');
|
|
522
623
|
}
|
|
523
624
|
} else if (!plan.isPaginated && !plan.hasBody && !plan.isDelete && op.queryParams.length > 0) {
|
|
524
625
|
// Non-paginated GET or void methods with query params get a typed options interface
|
|
@@ -628,12 +729,16 @@ function renderMethod(
|
|
|
628
729
|
if (op.description) docParts.push(op.description);
|
|
629
730
|
for (const param of op.pathParams) {
|
|
630
731
|
const paramName = fieldName(param.name);
|
|
631
|
-
|
|
732
|
+
// When the overlay folds a path param into the options object,
|
|
733
|
+
// document it as @param options.paramName instead of top-level.
|
|
734
|
+
const folded = validParamNames && !validParamNames.has(paramName) && validParamNames.has('options');
|
|
735
|
+
if (validParamNames && !validParamNames.has(paramName) && !folded) continue;
|
|
736
|
+
const docName = folded ? `options.${paramName}` : paramName;
|
|
632
737
|
const deprecatedPrefix = param.deprecated ? '(deprecated) ' : '';
|
|
633
738
|
if (param.description) {
|
|
634
|
-
docParts.push(`@param ${
|
|
739
|
+
docParts.push(`@param ${docName} - ${deprecatedPrefix}${param.description}`);
|
|
635
740
|
} else if (param.deprecated) {
|
|
636
|
-
docParts.push(`@param ${
|
|
741
|
+
docParts.push(`@param ${docName} - (deprecated)`);
|
|
637
742
|
}
|
|
638
743
|
if (param.default !== undefined) docParts.push(`@default ${JSON.stringify(param.default)}`);
|
|
639
744
|
if (param.example !== undefined) docParts.push(`@example ${JSON.stringify(param.example)}`);
|
|
@@ -660,36 +765,122 @@ function renderMethod(
|
|
|
660
765
|
}
|
|
661
766
|
// Skip header and cookie params in JSDoc — they are not exposed in the method signature.
|
|
662
767
|
// The SDK handles headers and cookies internally, so documenting them would be misleading.
|
|
663
|
-
// Document payload parameter when there is a request body
|
|
768
|
+
// Document payload/body parameter when there is a request body.
|
|
769
|
+
// Detect the actual param name from the overlay — the hand-written SDK may
|
|
770
|
+
// fold the body into an 'options' param instead of the generated 'payload'.
|
|
771
|
+
let bodyDocParamName: string | null = null;
|
|
664
772
|
if (plan.hasBody) {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
773
|
+
let bodyParamName = 'payload';
|
|
774
|
+
let overlayHadUnusableName = false;
|
|
775
|
+
if (validParamNames && !validParamNames.has('payload') && overlayMethod) {
|
|
776
|
+
const pathNames = new Set(op.pathParams.map((p) => fieldName(p.name)));
|
|
777
|
+
const candidates = overlayMethod.params.filter((p) => !pathNames.has(p.name) && p.name !== 'requestOptions');
|
|
778
|
+
// Filter out destructuring artifacts (e.g., __0) from extracted param names
|
|
779
|
+
const isUsableName = (name: string) => !/^__\d+$/.test(name);
|
|
780
|
+
if (candidates.length === 1 && isUsableName(candidates[0].name)) {
|
|
781
|
+
bodyParamName = candidates[0].name;
|
|
782
|
+
} else if (candidates.length === 1 && !isUsableName(candidates[0].name)) {
|
|
783
|
+
overlayHadUnusableName = true;
|
|
784
|
+
} else if (candidates.length > 1) {
|
|
785
|
+
// Multiple non-path params — match by type against the request body model
|
|
786
|
+
const bodyInfo = extractRequestBodyType(op, ctx);
|
|
787
|
+
if (bodyInfo?.kind === 'model') {
|
|
788
|
+
const bodyTypeName = resolveInterfaceName(bodyInfo.name, ctx);
|
|
789
|
+
const typeMatch = candidates.find((p) => p.type === bodyTypeName && isUsableName(p.name));
|
|
790
|
+
if (typeMatch) {
|
|
791
|
+
bodyParamName = typeMatch.name;
|
|
792
|
+
} else {
|
|
793
|
+
// Type names diverge (overlay vs spec). Fall back to matching
|
|
794
|
+
// overlay param names against body model field names so each
|
|
795
|
+
// extracted param gets documented individually.
|
|
796
|
+
const bodyModel = ctx.spec.models.find((m) => m.name === bodyInfo.name);
|
|
797
|
+
if (bodyModel) {
|
|
798
|
+
const fieldMap = new Map(bodyModel.fields.map((f) => [fieldName(f.name), f]));
|
|
799
|
+
for (const candidate of candidates) {
|
|
800
|
+
const matchingField = fieldMap.get(candidate.name);
|
|
801
|
+
if (matchingField?.description) {
|
|
802
|
+
docParts.push(`@param ${candidate.name} - ${matchingField.description}`);
|
|
803
|
+
if (matchingField.example !== undefined)
|
|
804
|
+
docParts.push(`@example ${JSON.stringify(matchingField.example)}`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
bodyDocParamName = '__multi__'; // prevent duplicate body doc below
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// When the overlay had an unusable param name (e.g., destructured __0),
|
|
815
|
+
// force documentation under 'payload' so the body isn't silently dropped.
|
|
816
|
+
if (
|
|
817
|
+
bodyDocParamName !== '__multi__' &&
|
|
818
|
+
(overlayHadUnusableName || !validParamNames || validParamNames.has(bodyParamName))
|
|
819
|
+
) {
|
|
820
|
+
bodyDocParamName = bodyParamName;
|
|
821
|
+
const bodyInfo = extractRequestBodyType(op, ctx);
|
|
822
|
+
if (bodyInfo?.kind === 'model') {
|
|
823
|
+
const bodyModel = ctx.spec.models.find((m) => m.name === bodyInfo.name);
|
|
824
|
+
let bodyDesc: string;
|
|
825
|
+
if (bodyModel?.description) {
|
|
826
|
+
bodyDesc = `@param ${bodyParamName} - ${bodyModel.description}`;
|
|
827
|
+
} else if (bodyModel) {
|
|
828
|
+
// When the model lacks a description, list its required fields to help
|
|
829
|
+
// callers understand what must be provided.
|
|
830
|
+
const requiredFieldNames = bodyModel.fields.filter((f) => f.required).map((f) => fieldName(f.name));
|
|
831
|
+
bodyDesc =
|
|
832
|
+
requiredFieldNames.length > 0
|
|
833
|
+
? `@param ${bodyParamName} - Object containing ${requiredFieldNames.join(', ')}.`
|
|
834
|
+
: `@param ${bodyParamName} - The request body.`;
|
|
835
|
+
} else {
|
|
836
|
+
bodyDesc = `@param ${bodyParamName} - The request body.`;
|
|
837
|
+
}
|
|
838
|
+
docParts.push(bodyDesc);
|
|
839
|
+
|
|
840
|
+
// When the body is folded into an options-style param (not 'payload'),
|
|
841
|
+
// expand individual fields so IDEs surface per-field documentation.
|
|
842
|
+
if (bodyParamName !== 'payload' && bodyModel) {
|
|
843
|
+
for (const bField of bodyModel.fields) {
|
|
844
|
+
const fName = `${bodyParamName}.${fieldName(bField.name)}`;
|
|
845
|
+
const deprecatedPrefix = bField.deprecated ? '(deprecated) ' : '';
|
|
846
|
+
if (bField.description) {
|
|
847
|
+
docParts.push(`@param ${fName} - ${deprecatedPrefix}${bField.description}`);
|
|
848
|
+
} else if (bField.deprecated) {
|
|
849
|
+
docParts.push(`@param ${fName} - (deprecated)`);
|
|
850
|
+
}
|
|
851
|
+
if (bField.example !== undefined) docParts.push(`@example ${JSON.stringify(bField.example)}`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
679
854
|
} else {
|
|
680
|
-
|
|
855
|
+
docParts.push(`@param ${bodyParamName} - The request body.`);
|
|
681
856
|
}
|
|
682
|
-
docParts.push(payloadDesc);
|
|
683
|
-
} else {
|
|
684
|
-
docParts.push('@param payload - The request body.');
|
|
685
857
|
}
|
|
686
858
|
}
|
|
687
859
|
// Document options parameter for paginated operations
|
|
860
|
+
const hasOptionsSummary = () => docParts.some((p) => /^@param options(\s|$)/.test(p));
|
|
688
861
|
if (plan.isPaginated) {
|
|
689
862
|
docParts.push('@param options - Pagination and filter options.');
|
|
690
|
-
} else if (op.queryParams.filter((q) => !hiddenParams.has(q.name)).length > 0) {
|
|
863
|
+
} else if (!hasOptionsSummary() && op.queryParams.filter((q) => !hiddenParams.has(q.name)).length > 0) {
|
|
691
864
|
docParts.push('@param options - Additional query options.');
|
|
692
865
|
}
|
|
866
|
+
// Ensure an @param options summary exists when there are @param options.xxx
|
|
867
|
+
// entries (from folded path/body params) or when the overlay exposes an
|
|
868
|
+
// options param that wasn't otherwise documented.
|
|
869
|
+
if (validParamNames?.has('options') && !hasOptionsSummary()) {
|
|
870
|
+
const hasOptionsDotEntries = docParts.some((p) => p.startsWith('@param options.'));
|
|
871
|
+
if (hasOptionsDotEntries || overlayMethod) {
|
|
872
|
+
docParts.push('@param options - The request options.');
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
// Reorder: ensure @param options summary comes before any @param options.xxx entries
|
|
876
|
+
{
|
|
877
|
+
const summaryIdx = docParts.findIndex((p) => /^@param options(\s|$)/.test(p));
|
|
878
|
+
const firstDotIdx = docParts.findIndex((p) => p.startsWith('@param options.'));
|
|
879
|
+
if (summaryIdx > firstDotIdx && firstDotIdx >= 0) {
|
|
880
|
+
const [summary] = docParts.splice(summaryIdx, 1);
|
|
881
|
+
docParts.splice(firstDotIdx, 0, summary);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
693
884
|
// @returns for the primary response model.
|
|
694
885
|
// When an overlay method exists, prefer its return type so the JSDoc
|
|
695
886
|
// matches the actual TypeScript signature (the overlay may use a
|
|
@@ -708,7 +899,8 @@ function renderMethod(
|
|
|
708
899
|
const itemTypeName = resolveInterfaceName(itemRawName, ctx);
|
|
709
900
|
docParts.push(`@returns {Promise<AutoPaginatable<${itemTypeName}>>}`);
|
|
710
901
|
} else if (responseModel) {
|
|
711
|
-
|
|
902
|
+
const returnTypeDoc = plan.isArrayResponse ? `${responseModel}[]` : responseModel;
|
|
903
|
+
docParts.push(`@returns {Promise<${returnTypeDoc}>}`);
|
|
712
904
|
} else {
|
|
713
905
|
docParts.push('@returns {Promise<void>}');
|
|
714
906
|
}
|
|
@@ -811,14 +1003,35 @@ function renderPaginatedMethod(
|
|
|
811
1003
|
): void {
|
|
812
1004
|
const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
|
|
813
1005
|
const optionsType = extraParams.length > 0 ? paginatedOptionsName(method, resolvedServiceName) : 'PaginationOptions';
|
|
1006
|
+
// When any extension param has a camelCase/snake_case divergence, the
|
|
1007
|
+
// resource file emits a `serialize<OptionsName>` helper — pass it to
|
|
1008
|
+
// createPaginatedList so the wire query uses snake_case keys.
|
|
1009
|
+
const needsWireSerializer = extraParams.some((p) => fieldName(p.name) !== wireFieldName(p.name));
|
|
1010
|
+
const serializerArg = needsWireSerializer ? `, serialize${optionsType}` : '';
|
|
814
1011
|
|
|
815
1012
|
const pathParams = buildPathParams(op, specEnumNames);
|
|
816
1013
|
const allParams = pathParams ? `${pathParams}, options?: ${optionsType}` : `options?: ${optionsType}`;
|
|
817
1014
|
|
|
1015
|
+
const wireType = wireInterfaceName(itemType);
|
|
1016
|
+
const serializeCall = serializerArg ? `options ? serialize${optionsType}(options) : undefined` : 'options';
|
|
1017
|
+
|
|
818
1018
|
lines.push(` async ${method}(${allParams}): Promise<AutoPaginatable<${itemType}, ${optionsType}>> {`);
|
|
819
|
-
lines.push(
|
|
820
|
-
|
|
821
|
-
);
|
|
1019
|
+
lines.push(` return new AutoPaginatable(`);
|
|
1020
|
+
lines.push(` await fetchAndDeserialize<${wireType}, ${itemType}>(`);
|
|
1021
|
+
lines.push(` this.workos,`);
|
|
1022
|
+
lines.push(` ${pathStr},`);
|
|
1023
|
+
lines.push(` deserialize${itemType},`);
|
|
1024
|
+
lines.push(` ${serializeCall},`);
|
|
1025
|
+
lines.push(` ),`);
|
|
1026
|
+
lines.push(` (params) =>`);
|
|
1027
|
+
lines.push(` fetchAndDeserialize<${wireType}, ${itemType}>(`);
|
|
1028
|
+
lines.push(` this.workos,`);
|
|
1029
|
+
lines.push(` ${pathStr},`);
|
|
1030
|
+
lines.push(` deserialize${itemType},`);
|
|
1031
|
+
lines.push(` params,`);
|
|
1032
|
+
lines.push(` ),`);
|
|
1033
|
+
lines.push(` options,`);
|
|
1034
|
+
lines.push(` );`);
|
|
822
1035
|
lines.push(' }');
|
|
823
1036
|
}
|
|
824
1037
|
|
|
@@ -926,16 +1139,22 @@ function renderBodyMethod(
|
|
|
926
1139
|
const encodingOption = encoding && encoding !== 'json' ? `, encoding: '${encoding}' as const` : '';
|
|
927
1140
|
const hasCustomEncoding = encodingOption !== '';
|
|
928
1141
|
|
|
929
|
-
|
|
1142
|
+
const returnType = plan.isArrayResponse ? `${responseModel}[]` : responseModel;
|
|
1143
|
+
const wireType = plan.isArrayResponse ? `${wireInterfaceName(responseModel)}[]` : wireInterfaceName(responseModel);
|
|
1144
|
+
const returnExpr = plan.isArrayResponse
|
|
1145
|
+
? `data.map(deserialize${responseModel})`
|
|
1146
|
+
: `deserialize${responseModel}(data)`;
|
|
1147
|
+
|
|
1148
|
+
lines.push(` async ${method}(${paramsStr}): Promise<${returnType}> {`);
|
|
930
1149
|
if (plan.isIdempotentPost) {
|
|
931
1150
|
if (hasCustomEncoding) {
|
|
932
|
-
lines.push(` const { data } = await this.workos.${op.httpMethod}<${
|
|
1151
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(`);
|
|
933
1152
|
lines.push(` ${pathStr},`);
|
|
934
1153
|
lines.push(` ${bodyExpr},`);
|
|
935
1154
|
lines.push(` { ...requestOptions${encodingOption} },`);
|
|
936
1155
|
lines.push(' );');
|
|
937
1156
|
} else {
|
|
938
|
-
lines.push(` const { data } = await this.workos.${op.httpMethod}<${
|
|
1157
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(`);
|
|
939
1158
|
lines.push(` ${pathStr},`);
|
|
940
1159
|
lines.push(` ${bodyExpr},`);
|
|
941
1160
|
lines.push(' requestOptions,');
|
|
@@ -943,19 +1162,19 @@ function renderBodyMethod(
|
|
|
943
1162
|
}
|
|
944
1163
|
} else {
|
|
945
1164
|
if (hasCustomEncoding) {
|
|
946
|
-
lines.push(` const { data } = await this.workos.${op.httpMethod}<${
|
|
1165
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(`);
|
|
947
1166
|
lines.push(` ${pathStr},`);
|
|
948
1167
|
lines.push(` ${bodyExpr},`);
|
|
949
1168
|
lines.push(` { ${encodingOption.slice(2)} },`);
|
|
950
1169
|
lines.push(' );');
|
|
951
1170
|
} else {
|
|
952
|
-
lines.push(` const { data } = await this.workos.${op.httpMethod}<${
|
|
1171
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(`);
|
|
953
1172
|
lines.push(` ${pathStr},`);
|
|
954
1173
|
lines.push(` ${bodyExpr},`);
|
|
955
1174
|
lines.push(' );');
|
|
956
1175
|
}
|
|
957
1176
|
}
|
|
958
|
-
lines.push(` return
|
|
1177
|
+
lines.push(` return ${returnExpr};`);
|
|
959
1178
|
lines.push(' }');
|
|
960
1179
|
}
|
|
961
1180
|
|
|
@@ -989,7 +1208,13 @@ function renderGetMethod(
|
|
|
989
1208
|
: `options?: ${optionsType}`
|
|
990
1209
|
: params;
|
|
991
1210
|
|
|
992
|
-
|
|
1211
|
+
const returnType = plan.isArrayResponse ? `${responseModel}[]` : responseModel;
|
|
1212
|
+
const wireType = plan.isArrayResponse ? `${wireInterfaceName(responseModel)}[]` : wireInterfaceName(responseModel);
|
|
1213
|
+
const returnExpr = plan.isArrayResponse
|
|
1214
|
+
? `data.map(deserialize${responseModel})`
|
|
1215
|
+
: `deserialize${responseModel}(data)`;
|
|
1216
|
+
|
|
1217
|
+
lines.push(` async ${method}(${allParams}): Promise<${returnType}> {`);
|
|
993
1218
|
if (hasQuery) {
|
|
994
1219
|
if (hasInjected) {
|
|
995
1220
|
// Build the query object with visible params, defaults, and inferred fields
|
|
@@ -1026,30 +1251,22 @@ function renderGetMethod(
|
|
|
1026
1251
|
queryParts.push(`${field}: ${clientFieldExpression(field)}`);
|
|
1027
1252
|
}
|
|
1028
1253
|
|
|
1029
|
-
lines.push(
|
|
1030
|
-
` const { data } = await this.workos.${op.httpMethod}<${wireInterfaceName(responseModel)}>(${pathStr}, {`,
|
|
1031
|
-
);
|
|
1254
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr}, {`);
|
|
1032
1255
|
lines.push(` query: { ${queryParts.join(', ')} },`);
|
|
1033
1256
|
lines.push(' });');
|
|
1034
1257
|
} else {
|
|
1035
1258
|
const queryExpr = renderQueryExpr(visibleQueryParams);
|
|
1036
|
-
lines.push(
|
|
1037
|
-
` const { data } = await this.workos.${op.httpMethod}<${wireInterfaceName(responseModel)}>(${pathStr}, {`,
|
|
1038
|
-
);
|
|
1259
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr}, {`);
|
|
1039
1260
|
lines.push(` query: ${queryExpr},`);
|
|
1040
1261
|
lines.push(' });');
|
|
1041
1262
|
}
|
|
1042
1263
|
} else if (httpMethodNeedsBody(op.httpMethod)) {
|
|
1043
1264
|
// PUT/PATCH/POST require a body argument even when the spec has no request body
|
|
1044
|
-
lines.push(
|
|
1045
|
-
` const { data } = await this.workos.${op.httpMethod}<${wireInterfaceName(responseModel)}>(${pathStr}, {});`,
|
|
1046
|
-
);
|
|
1265
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr}, {});`);
|
|
1047
1266
|
} else {
|
|
1048
|
-
lines.push(
|
|
1049
|
-
` const { data } = await this.workos.${op.httpMethod}<${wireInterfaceName(responseModel)}>(${pathStr});`,
|
|
1050
|
-
);
|
|
1267
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr});`);
|
|
1051
1268
|
}
|
|
1052
|
-
lines.push(` return
|
|
1269
|
+
lines.push(` return ${returnExpr};`);
|
|
1053
1270
|
lines.push(' }');
|
|
1054
1271
|
}
|
|
1055
1272
|
|
|
@@ -1263,9 +1480,16 @@ function renderUnionBodySerializer(
|
|
|
1263
1480
|
const cases: string[] = [];
|
|
1264
1481
|
for (const [value, modelName] of Object.entries(disc.mapping)) {
|
|
1265
1482
|
const resolved = resolveInterfaceName(modelName, ctx);
|
|
1266
|
-
|
|
1483
|
+
// Switch on a typed discriminator narrows `payload` to the variant, so the
|
|
1484
|
+
// serializer call type-checks without any casts.
|
|
1485
|
+
cases.push(`case '${value}': return serialize${resolved}(payload)`);
|
|
1267
1486
|
}
|
|
1268
|
-
|
|
1487
|
+
// Assign `payload` to `never` in the default branch to get a compile-time
|
|
1488
|
+
// exhaustiveness check — if a new variant is added to the union but not to
|
|
1489
|
+
// the switch, the build fails here. At runtime, we still throw so an
|
|
1490
|
+
// unknown discriminator slipping through via `as any` fails loudly rather
|
|
1491
|
+
// than silently forwarding camelCase to the API.
|
|
1492
|
+
return `(() => { switch (payload.${prop}) { ${cases.join('; ')}; default: { const _unknown: never = payload; throw new Error(\`Unknown ${prop}: \${(_unknown as { ${prop}?: unknown }).${prop}}\`) } } })()`;
|
|
1269
1493
|
}
|
|
1270
1494
|
|
|
1271
1495
|
/**
|