@workos/oagen-emitters 0.12.1 → 0.12.3
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-pr-title.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/.node-version +1 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +14 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{plugin-CmfzawTp.mjs → plugin-D2N2ZT5W.mjs} +2566 -1493
- package/dist/plugin-D2N2ZT5W.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +6 -6
- package/renovate.json +46 -6
- package/src/node/client.ts +19 -32
- package/src/node/enums.ts +67 -30
- package/src/node/errors.ts +2 -8
- package/src/node/field-plan.ts +188 -52
- package/src/node/fixtures.ts +11 -33
- package/src/node/index.ts +354 -20
- package/src/node/live-surface.ts +378 -0
- package/src/node/models.ts +547 -351
- package/src/node/naming.ts +122 -25
- package/src/node/node-overrides.ts +77 -0
- package/src/node/options.ts +41 -0
- package/src/node/path-expression.ts +11 -4
- package/src/node/resources.ts +473 -48
- package/src/node/sdk-errors.ts +0 -16
- package/src/node/tests.ts +152 -93
- package/src/node/type-map.ts +40 -18
- package/src/node/utils.ts +89 -102
- package/src/node/wrappers.ts +0 -20
- package/test/node/client.test.ts +106 -1201
- package/test/node/enums.test.ts +59 -130
- package/test/node/errors.test.ts +2 -3
- package/test/node/live-surface.test.ts +240 -0
- package/test/node/models.test.ts +396 -765
- package/test/node/naming.test.ts +69 -234
- package/test/node/resources.test.ts +435 -2025
- package/test/node/tests.test.ts +214 -0
- package/test/node/type-map.test.ts +49 -54
- package/test/node/utils.test.ts +29 -80
- package/dist/plugin-CmfzawTp.mjs.map +0 -1
- package/test/node/serializers.test.ts +0 -444
package/src/node/resources.ts
CHANGED
|
@@ -11,7 +11,39 @@ import type {
|
|
|
11
11
|
} from '@workos/oagen';
|
|
12
12
|
import { planOperation, toPascalCase, toCamelCase } from '@workos/oagen';
|
|
13
13
|
import type { OperationPlan } from '@workos/oagen';
|
|
14
|
-
import { mapTypeRef } from './type-map.js';
|
|
14
|
+
import { mapTypeRef, isInlineEnum } from './type-map.js';
|
|
15
|
+
import { liveSurfaceHasFunction, liveSurfaceHasFile, liveSurfaceHasAutogenFile } from './live-surface.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Render the request-body argument for an HTTP call.
|
|
19
|
+
*
|
|
20
|
+
* Three cases:
|
|
21
|
+
* 1. `serialize${T}` is in the live SDK's serializer functions → call it.
|
|
22
|
+
* 2. The serializer file exists on disk but does *not* export `serialize${T}`
|
|
23
|
+
* (e.g. workos-node's `validate-api-key.serializer.ts` ships only the
|
|
24
|
+
* response deserializer). The user owns the file; we cannot add to it,
|
|
25
|
+
* so pass `payload` straight through. `*Options` interfaces in
|
|
26
|
+
* workos-node are already wire-shaped (primitives or pre-snake_cased
|
|
27
|
+
* keys), so this preserves correctness for the workos-node convention.
|
|
28
|
+
* 3. Otherwise the emitter is producing the serializer this run → call.
|
|
29
|
+
*/
|
|
30
|
+
function bodyArgExpr(irModelName: string, resolvedName: string, ctx: EmitterContext, paramName = 'payload'): string {
|
|
31
|
+
const ser = `serialize${resolvedName}`;
|
|
32
|
+
if (liveSurfaceHasFunction(ser)) return `${ser}(${paramName})`;
|
|
33
|
+
|
|
34
|
+
const sourceFile = (ctx.apiSurface?.interfaces?.[resolvedName] as { sourceFile?: string } | undefined)?.sourceFile;
|
|
35
|
+
const candidate = sourceFile
|
|
36
|
+
? sourceFile.replace('/interfaces/', '/serializers/').replace('.interface.ts', '.serializer.ts')
|
|
37
|
+
: `src/${defaultModelDir(irModelName, ctx)}/serializers/${fileName(irModelName)}.serializer.ts`;
|
|
38
|
+
if (liveSurfaceHasFile(candidate) && !liveSurfaceHasAutogenFile(candidate)) return paramName;
|
|
39
|
+
|
|
40
|
+
return `${ser}(${paramName})`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function defaultModelDir(irModelName: string, ctx: EmitterContext): string {
|
|
44
|
+
const { modelToService, resolveDir } = createServiceDirResolver(ctx.spec.models, ctx.spec.services, ctx);
|
|
45
|
+
return resolveDir(modelToService.get(irModelName));
|
|
46
|
+
}
|
|
15
47
|
import {
|
|
16
48
|
fieldName,
|
|
17
49
|
wireFieldName,
|
|
@@ -28,6 +60,7 @@ import {
|
|
|
28
60
|
isServiceCoveredByExisting,
|
|
29
61
|
hasMethodsAbsentFromBaseline,
|
|
30
62
|
uncoveredOperations,
|
|
63
|
+
relativeImport,
|
|
31
64
|
} from './utils.js';
|
|
32
65
|
import { assignEnumsToServices } from './enums.js';
|
|
33
66
|
import { unwrapListModel } from './fixtures.js';
|
|
@@ -42,6 +75,7 @@ import {
|
|
|
42
75
|
import { generateWrapperMethods, collectWrapperResponseModels } from './wrappers.js';
|
|
43
76
|
import { buildNodePathExpression } from './path-expression.js';
|
|
44
77
|
import { resolveWrapperParams } from '../shared/wrapper-utils.js';
|
|
78
|
+
import { isNodeOwnedService } from './options.js';
|
|
45
79
|
|
|
46
80
|
/**
|
|
47
81
|
* Check whether the baseline (hand-written) class has a constructor compatible
|
|
@@ -79,6 +113,12 @@ export function resolveResourceClassName(service: Service, ctx: EmitterContext):
|
|
|
79
113
|
return irName;
|
|
80
114
|
}
|
|
81
115
|
|
|
116
|
+
export function resolveResourceDir(service: Service, ctx: EmitterContext): string {
|
|
117
|
+
const resolvedName = resolveResourceClassName(service, ctx);
|
|
118
|
+
if (resolvedName === 'WebhooksEndpoints') return 'webhooks';
|
|
119
|
+
return resolveServiceDir(resolvedName);
|
|
120
|
+
}
|
|
121
|
+
|
|
82
122
|
/** Standard pagination query params handled by PaginationOptions — not imported individually. */
|
|
83
123
|
const PAGINATION_PARAM_NAMES = new Set(['limit', 'before', 'after', 'order']);
|
|
84
124
|
|
|
@@ -99,6 +139,76 @@ function paginatedOptionsName(method: string, resolvedServiceName: string): stri
|
|
|
99
139
|
return toPascalCase(method) + 'Options';
|
|
100
140
|
}
|
|
101
141
|
|
|
142
|
+
type BaselineMethod = {
|
|
143
|
+
params: Array<{ name: string; type: string; optional?: boolean; passingStyle?: string }>;
|
|
144
|
+
returnType?: string;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
function baselineMethodFor(service: Service, method: string, ctx: EmitterContext): BaselineMethod | undefined {
|
|
148
|
+
const serviceClass = resolveResourceClassName(service, ctx);
|
|
149
|
+
return ctx.apiSurface?.classes?.[serviceClass]?.methods?.[method]?.[0] as BaselineMethod | undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function optionsObjectParam(method: BaselineMethod | undefined): { name: string; type: string } | undefined {
|
|
153
|
+
if (!method || method.params.length !== 1) return undefined;
|
|
154
|
+
const [param] = method.params;
|
|
155
|
+
if (param.name !== 'options') return undefined;
|
|
156
|
+
if (param.passingStyle && param.passingStyle !== 'options_object') return undefined;
|
|
157
|
+
if (!param.type || /^(Record|object|any|unknown)\b/.test(param.type)) return undefined;
|
|
158
|
+
return { name: param.name, type: param.type };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function autoPaginatableItemType(returnType: string | undefined): string | undefined {
|
|
162
|
+
return returnType?.match(/\bAutoPaginatable<\s*([A-Za-z_$][\w$]*)/)?.[1];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function baselineTypeSourceFile(ctx: EmitterContext, typeName: string): string | undefined {
|
|
166
|
+
const surface = ctx.apiSurface as
|
|
167
|
+
| {
|
|
168
|
+
interfaces?: Record<string, { sourceFile?: string }>;
|
|
169
|
+
typeAliases?: Record<string, { sourceFile?: string }>;
|
|
170
|
+
enums?: Record<string, { sourceFile?: string }>;
|
|
171
|
+
}
|
|
172
|
+
| undefined;
|
|
173
|
+
return (
|
|
174
|
+
surface?.interfaces?.[typeName]?.sourceFile ??
|
|
175
|
+
surface?.typeAliases?.[typeName]?.sourceFile ??
|
|
176
|
+
surface?.enums?.[typeName]?.sourceFile
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function preferredBaselineTypeName(ctx: EmitterContext, typeName: string | undefined): string | undefined {
|
|
181
|
+
if (!typeName) return undefined;
|
|
182
|
+
const surface = ctx.apiSurface as
|
|
183
|
+
| {
|
|
184
|
+
typeAliases?: Record<string, { value?: string; sourceFile?: string }>;
|
|
185
|
+
interfaces?: Record<string, { sourceFile?: string }>;
|
|
186
|
+
}
|
|
187
|
+
| undefined;
|
|
188
|
+
const sourceFile = baselineTypeSourceFile(ctx, typeName);
|
|
189
|
+
for (const [alias, info] of Object.entries(surface?.typeAliases ?? {})) {
|
|
190
|
+
if (info.value !== typeName) continue;
|
|
191
|
+
if (sourceFile && info.sourceFile !== sourceFile) continue;
|
|
192
|
+
if (liveSurfaceHasFunction(`deserialize${alias}`)) return alias;
|
|
193
|
+
}
|
|
194
|
+
return typeName;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function preferredBaselineReturnType(ctx: EmitterContext, returnType: string | undefined): string | undefined {
|
|
198
|
+
const itemType = autoPaginatableItemType(returnType);
|
|
199
|
+
const preferred = preferredBaselineTypeName(ctx, itemType);
|
|
200
|
+
if (!returnType || !itemType || !preferred || preferred === itemType) return returnType;
|
|
201
|
+
return returnType.replace(new RegExp(`\\b${itemType}\\b`, 'g'), preferred);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function requestEntityType(bodyExpr: string, requestType: string): string {
|
|
205
|
+
return bodyExpr === 'payload' ? requestType : wireInterfaceName(requestType);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function unionEntityType(modelNames: string[], ctx: EmitterContext): string {
|
|
209
|
+
return modelNames.map((name) => wireInterfaceName(resolveInterfaceName(name, ctx))).join(' | ');
|
|
210
|
+
}
|
|
211
|
+
|
|
102
212
|
/** HTTP methods that require a body argument even when the spec has no request body. */
|
|
103
213
|
function httpMethodNeedsBody(method: string): boolean {
|
|
104
214
|
return method === 'post' || method === 'put' || method === 'patch';
|
|
@@ -209,7 +319,7 @@ function generatePaginatedOptionsInterfaces(
|
|
|
209
319
|
): GeneratedFile[] {
|
|
210
320
|
const files: GeneratedFile[] = [];
|
|
211
321
|
const resolvedName = resolveResourceClassName(service, ctx);
|
|
212
|
-
const serviceDir =
|
|
322
|
+
const serviceDir = resolveResourceDir(service, ctx);
|
|
213
323
|
|
|
214
324
|
const plans = service.operations.map((op) => ({
|
|
215
325
|
op,
|
|
@@ -263,12 +373,14 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
263
373
|
const topLevelEnumNames = new Set(ctx.spec.enums.map((e) => e.name));
|
|
264
374
|
|
|
265
375
|
for (const service of mergedServices) {
|
|
266
|
-
|
|
376
|
+
const isOwnedService = isNodeOwnedService(ctx, service.name, resolveResourceClassName(service, ctx));
|
|
377
|
+
if (!isOwnedService && isServiceCoveredByExisting(service, ctx)) {
|
|
267
378
|
if (!hasMethodsAbsentFromBaseline(service, ctx)) {
|
|
268
379
|
continue; // Fully covered, no new methods -- skip entirely
|
|
269
380
|
}
|
|
270
381
|
// Partially covered -- has new methods that need to be added.
|
|
271
382
|
const file = generateResourceClass(service, ctx);
|
|
383
|
+
if (!file) continue;
|
|
272
384
|
delete file.skipIfExists;
|
|
273
385
|
// Suppress auto-generated header — the file is a merge target
|
|
274
386
|
// containing hand-written code, not a fully generated file.
|
|
@@ -277,7 +389,7 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
277
389
|
continue;
|
|
278
390
|
}
|
|
279
391
|
|
|
280
|
-
const ops = uncoveredOperations(service, ctx);
|
|
392
|
+
const ops = isOwnedService ? service.operations : uncoveredOperations(service, ctx);
|
|
281
393
|
if (ops.length === 0) continue;
|
|
282
394
|
|
|
283
395
|
if (ops.length < service.operations.length) {
|
|
@@ -285,6 +397,7 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
285
397
|
// for both covered and uncovered methods. Remove skipIfExists so the
|
|
286
398
|
// merger adds new methods AND refreshes existing JSDoc.
|
|
287
399
|
const file = generateResourceClass(service, ctx);
|
|
400
|
+
if (!file) continue;
|
|
288
401
|
delete file.skipIfExists;
|
|
289
402
|
// Suppress auto-generated header — the file is a merge target
|
|
290
403
|
// containing hand-written code, not a fully generated file.
|
|
@@ -296,6 +409,7 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
296
409
|
// overwrites — emitter improvements (serializer dispatch, JSDoc, etc.)
|
|
297
410
|
// must propagate without manual intervention.
|
|
298
411
|
const file = generateResourceClass(service, ctx);
|
|
412
|
+
if (!file) continue;
|
|
299
413
|
delete file.skipIfExists;
|
|
300
414
|
files.push(file);
|
|
301
415
|
}
|
|
@@ -306,20 +420,22 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
306
420
|
// stable. Placing them under `interfaces/` lets the per-service barrel
|
|
307
421
|
// pick them up automatically.
|
|
308
422
|
for (const service of mergedServices) {
|
|
309
|
-
|
|
423
|
+
const isOwnedService = isNodeOwnedService(ctx, service.name, resolveResourceClassName(service, ctx));
|
|
424
|
+
if (!isOwnedService && isServiceCoveredByExisting(service, ctx) && !hasMethodsAbsentFromBaseline(service, ctx))
|
|
425
|
+
continue;
|
|
310
426
|
files.push(...generatePaginatedOptionsInterfaces(service, ctx, topLevelEnumNames));
|
|
311
427
|
}
|
|
312
428
|
|
|
313
429
|
return files;
|
|
314
430
|
}
|
|
315
431
|
|
|
316
|
-
function generateResourceClass(service: Service, ctx: EmitterContext): GeneratedFile {
|
|
432
|
+
function generateResourceClass(service: Service, ctx: EmitterContext): GeneratedFile | null {
|
|
317
433
|
const resolvedName = resolveResourceClassName(service, ctx);
|
|
318
|
-
const serviceDir =
|
|
434
|
+
const serviceDir = resolveResourceDir(service, ctx);
|
|
319
435
|
const serviceClass = resolvedName;
|
|
320
436
|
const resourcePath = `src/${serviceDir}/${fileName(resolvedName)}.ts`;
|
|
321
437
|
|
|
322
|
-
|
|
438
|
+
let plans = service.operations.map((op) => ({
|
|
323
439
|
op,
|
|
324
440
|
plan: planOperation(op),
|
|
325
441
|
method: resolveMethodName(op, service, ctx),
|
|
@@ -362,28 +478,60 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
362
478
|
}
|
|
363
479
|
}
|
|
364
480
|
|
|
481
|
+
// Filter out operations whose method name already exists in the baseline
|
|
482
|
+
// class. The user's `@oagen-ignore-start`/`@oagen-ignore-end` blocks in
|
|
483
|
+
// the existing file preserve those method bodies; emitting them again
|
|
484
|
+
// would duplicate the symbol after the engine appends the preserved block
|
|
485
|
+
// (engine writeFiles → overwriteWithPreservedRegions appends, doesn't
|
|
486
|
+
// replace, when the block is method-level not class-level).
|
|
487
|
+
//
|
|
488
|
+
// Direct check against `apiSurface.classes[serviceClass].methods` rather
|
|
489
|
+
// than `uncoveredOperations`, which routes through `overlayLookup` that
|
|
490
|
+
// isn't always populated.
|
|
491
|
+
//
|
|
492
|
+
// Done after dedup + sort so the rendered methods keep their stable names.
|
|
493
|
+
// Imports below filter through `plans`, so they automatically narrow to
|
|
494
|
+
// the kept operations only — no orphan imports.
|
|
495
|
+
const baselineMethodNames = new Set(Object.keys(ctx.apiSurface?.classes?.[serviceClass]?.methods ?? {}));
|
|
496
|
+
const planCountBeforeFilter = plans.length;
|
|
497
|
+
if (!isNodeOwnedService(ctx, service.name, serviceClass) && baselineMethodNames.size > 0) {
|
|
498
|
+
plans = plans.filter((p) => !baselineMethodNames.has(p.method));
|
|
499
|
+
}
|
|
500
|
+
const filteredOut = planCountBeforeFilter - plans.length;
|
|
501
|
+
|
|
502
|
+
// Skip emitting the file altogether when every operation already exists in
|
|
503
|
+
// baseline AND the existing class is preserved via `@oagen-ignore-start`
|
|
504
|
+
// blocks. The autogen-overwrite path would otherwise write an empty class
|
|
505
|
+
// shell that the engine's region preservation would have to reconstruct.
|
|
506
|
+
if (plans.length === 0 && filteredOut > 0) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
365
510
|
const hasPaginated = plans.some((p) => p.plan.isPaginated);
|
|
511
|
+
const needsPaginationOptionsImport = plans.some(
|
|
512
|
+
(p) =>
|
|
513
|
+
p.plan.isPaginated &&
|
|
514
|
+
(!optionsObjectParam(baselineMethodFor(service, p.method, ctx)) ||
|
|
515
|
+
/\bPaginationOptions\b/.test(
|
|
516
|
+
preferredBaselineReturnType(ctx, baselineMethodFor(service, p.method, ctx)?.returnType) ?? '',
|
|
517
|
+
)),
|
|
518
|
+
);
|
|
366
519
|
const modelMap = new Map(ctx.spec.models.map((m) => [m.name, m]));
|
|
367
520
|
|
|
368
|
-
// When merging into an existing class, the merger keeps baseline method
|
|
369
|
-
// bodies but may add imports from the generated code. To avoid orphaned
|
|
370
|
-
// imports for types used only by baseline methods (whose bodies are kept
|
|
371
|
-
// intact), skip model collection for methods that already exist.
|
|
372
|
-
const baselineMethodSet = new Set<string>();
|
|
373
|
-
const baselineClass = ctx.apiSurface?.classes?.[serviceClass];
|
|
374
|
-
if (baselineClass?.methods) {
|
|
375
|
-
for (const name of Object.keys(baselineClass.methods)) {
|
|
376
|
-
baselineMethodSet.add(name);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
521
|
// Collect models for imports — only include models that are actually used
|
|
381
522
|
// in method signatures (not all union variants from the spec)
|
|
382
523
|
const responseModels = new Set<string>();
|
|
383
524
|
const requestModels = new Set<string>();
|
|
525
|
+
const requestModelsForSignature = new Set<string>();
|
|
384
526
|
const paramEnums = new Set<string>();
|
|
385
527
|
const paramModels = new Set<string>();
|
|
386
|
-
|
|
528
|
+
const optionObjectTypes = new Set<string>();
|
|
529
|
+
const baselineResponseTypes = new Set<string>();
|
|
530
|
+
for (const { op, plan } of plans) {
|
|
531
|
+
const baselineMethod = baselineMethodFor(service, resolveMethodName(op, service, ctx), ctx);
|
|
532
|
+
const existingOptions = optionsObjectParam(baselineMethod);
|
|
533
|
+
if (existingOptions) optionObjectTypes.add(existingOptions.type);
|
|
534
|
+
|
|
387
535
|
// Always collect param type refs for enums — inline options interfaces
|
|
388
536
|
// are generated for all methods (including baseline ones), so their
|
|
389
537
|
// type dependencies must always be imported.
|
|
@@ -394,12 +542,20 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
394
542
|
collectParamTypeRefs(param.type, paramEnums, paramModels);
|
|
395
543
|
}
|
|
396
544
|
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
545
|
+
// Always collect imports for every rendered method. Earlier versions
|
|
546
|
+
// skipped baseline methods on the assumption the AST merger would keep
|
|
547
|
+
// their existing imports — but the autogen-aware writer in
|
|
548
|
+
// `node/index.ts` overwrites previously-generated files in full so spec
|
|
549
|
+
// renames propagate. With overwrite, missing imports become real
|
|
550
|
+
// compile errors. Redundant imports are harmless (eslint --fix prunes
|
|
551
|
+
// them post-generation via `formatCommand`).
|
|
552
|
+
|
|
553
|
+
const baselinePaginatedItemType = existingOptions
|
|
554
|
+
? preferredBaselineTypeName(ctx, autoPaginatableItemType(baselineMethod?.returnType))
|
|
555
|
+
: undefined;
|
|
556
|
+
if (plan.isPaginated && baselinePaginatedItemType) {
|
|
557
|
+
baselineResponseTypes.add(baselinePaginatedItemType);
|
|
558
|
+
} else if (plan.isPaginated && op.pagination?.itemType.kind === 'model') {
|
|
403
559
|
// For paginated operations, import the item type (e.g., Connection)
|
|
404
560
|
// rather than the list wrapper type (e.g., ConnectionList).
|
|
405
561
|
// fetchAndDeserialize handles the list envelope internally.
|
|
@@ -420,18 +576,21 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
420
576
|
const bodyInfo = extractRequestBodyType(op, ctx);
|
|
421
577
|
if (bodyInfo?.kind === 'model') {
|
|
422
578
|
requestModels.add(bodyInfo.name);
|
|
579
|
+
if (!existingOptions) requestModelsForSignature.add(bodyInfo.name);
|
|
423
580
|
} else if (bodyInfo?.kind === 'union') {
|
|
424
581
|
if (bodyInfo.discriminator) {
|
|
425
582
|
// Discriminated union: import variant models with serializers so we can
|
|
426
583
|
// dispatch to the correct serializer at runtime based on the discriminator.
|
|
427
584
|
for (const name of bodyInfo.modelNames) {
|
|
428
585
|
requestModels.add(name);
|
|
586
|
+
if (!existingOptions) requestModelsForSignature.add(name);
|
|
429
587
|
}
|
|
430
588
|
} else {
|
|
431
589
|
// Non-discriminated union: import variant models with serializers so we
|
|
432
590
|
// can dispatch to the correct serializer at runtime via field guards.
|
|
433
591
|
for (const name of bodyInfo.modelNames) {
|
|
434
592
|
requestModels.add(name);
|
|
593
|
+
if (!existingOptions) requestModelsForSignature.add(name);
|
|
435
594
|
}
|
|
436
595
|
}
|
|
437
596
|
}
|
|
@@ -443,8 +602,7 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
443
602
|
// `redirect_uris: RedirectUriInput[]`) — otherwise the wrapper emits a
|
|
444
603
|
// reference to a type it never imported.
|
|
445
604
|
const resolvedLookup = buildResolvedLookup(ctx);
|
|
446
|
-
for (const { op
|
|
447
|
-
if (baselineMethodSet.has(method)) continue;
|
|
605
|
+
for (const { op } of plans) {
|
|
448
606
|
const resolved = lookupResolved(op, resolvedLookup);
|
|
449
607
|
if (resolved) {
|
|
450
608
|
for (const name of collectWrapperResponseModels(resolved)) {
|
|
@@ -458,14 +616,16 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
458
616
|
}
|
|
459
617
|
}
|
|
460
618
|
|
|
461
|
-
const allModels = new Set([...responseModels, ...
|
|
619
|
+
const allModels = new Set([...responseModels, ...requestModelsForSignature, ...paramModels]);
|
|
462
620
|
|
|
463
621
|
const lines: string[] = [];
|
|
464
622
|
|
|
465
623
|
// Imports
|
|
466
624
|
lines.push("import type { WorkOS } from '../workos';");
|
|
467
625
|
if (hasPaginated) {
|
|
468
|
-
|
|
626
|
+
if (needsPaginationOptionsImport) {
|
|
627
|
+
lines.push("import type { PaginationOptions } from '../common/interfaces/pagination-options.interface';");
|
|
628
|
+
}
|
|
469
629
|
lines.push("import { AutoPaginatable } from '../common/utils/pagination';");
|
|
470
630
|
lines.push("import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize';");
|
|
471
631
|
}
|
|
@@ -490,6 +650,13 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
490
650
|
lines.push("import type { PostOptions } from '../common/interfaces/post-options.interface';");
|
|
491
651
|
}
|
|
492
652
|
|
|
653
|
+
const importedTypeNames = new Set<string>();
|
|
654
|
+
for (const optionType of optionObjectTypes) {
|
|
655
|
+
if (importedTypeNames.has(optionType)) continue;
|
|
656
|
+
importedTypeNames.add(optionType);
|
|
657
|
+
lines.push(`import type { ${optionType} } from './interfaces/${fileName(optionType)}.interface';`);
|
|
658
|
+
}
|
|
659
|
+
|
|
493
660
|
// Compute model-to-service mapping for correct cross-service import paths
|
|
494
661
|
const { modelToService, resolveDir } = createServiceDirResolver(ctx.spec.models, ctx.spec.services, ctx);
|
|
495
662
|
|
|
@@ -499,9 +666,15 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
499
666
|
for (const name of responseModels) {
|
|
500
667
|
usedWireTypes.add(resolveInterfaceName(name, ctx));
|
|
501
668
|
}
|
|
669
|
+
for (const name of requestModels) {
|
|
670
|
+
const resolved = resolveInterfaceName(name, ctx);
|
|
671
|
+
const bodyExpr = bodyArgExpr(name, resolved, ctx);
|
|
672
|
+
if (requestEntityType(bodyExpr, resolved) !== resolved) {
|
|
673
|
+
usedWireTypes.add(resolved);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
502
676
|
|
|
503
677
|
// Track imported resolved names to prevent duplicate type name collisions
|
|
504
|
-
const importedTypeNames = new Set<string>();
|
|
505
678
|
for (const name of allModels) {
|
|
506
679
|
const resolved = resolveInterfaceName(name, ctx);
|
|
507
680
|
if (importedTypeNames.has(resolved)) continue; // Skip duplicate resolved names
|
|
@@ -519,6 +692,33 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
519
692
|
}
|
|
520
693
|
}
|
|
521
694
|
|
|
695
|
+
for (const name of requestModels) {
|
|
696
|
+
if (allModels.has(name)) continue;
|
|
697
|
+
const resolved = resolveInterfaceName(name, ctx);
|
|
698
|
+
if (!usedWireTypes.has(resolved)) continue;
|
|
699
|
+
const wireName = wireInterfaceName(resolved);
|
|
700
|
+
if (importedTypeNames.has(wireName)) continue;
|
|
701
|
+
const modelDir = modelToService.get(name);
|
|
702
|
+
const modelServiceDir = resolveDir(modelDir);
|
|
703
|
+
const relPath =
|
|
704
|
+
modelServiceDir === serviceDir
|
|
705
|
+
? `./interfaces/${fileName(name)}.interface`
|
|
706
|
+
: `../${modelServiceDir}/interfaces/${fileName(name)}.interface`;
|
|
707
|
+
lines.push(`import type { ${wireName} } from '${relPath}';`);
|
|
708
|
+
importedTypeNames.add(wireName);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
for (const name of baselineResponseTypes) {
|
|
712
|
+
if (importedTypeNames.has(name)) continue;
|
|
713
|
+
const wireName = wireInterfaceName(name);
|
|
714
|
+
const sourceFile = baselineTypeSourceFile(ctx, name) ?? baselineTypeSourceFile(ctx, wireName);
|
|
715
|
+
if (!sourceFile) continue;
|
|
716
|
+
const importNames = wireName === name ? name : `${name}, ${wireName}`;
|
|
717
|
+
lines.push(`import type { ${importNames} } from '${relativeImport(resourcePath, sourceFile)}';`);
|
|
718
|
+
importedTypeNames.add(name);
|
|
719
|
+
importedTypeNames.add(wireName);
|
|
720
|
+
}
|
|
721
|
+
|
|
522
722
|
// Collect serializer imports by module path so we can merge deserialize and
|
|
523
723
|
// serialize imports from the same module into a single import statement.
|
|
524
724
|
const serializerImportsByPath = new Map<string, string[]>();
|
|
@@ -538,12 +738,38 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
538
738
|
existing.push(`deserialize${resolved}`);
|
|
539
739
|
serializerImportsByPath.set(relPath, existing);
|
|
540
740
|
}
|
|
741
|
+
for (const name of baselineResponseTypes) {
|
|
742
|
+
if (importedDeserializers.has(name)) continue;
|
|
743
|
+
importedDeserializers.add(name);
|
|
744
|
+
const sourceFile = baselineTypeSourceFile(ctx, name) ?? baselineTypeSourceFile(ctx, wireInterfaceName(name));
|
|
745
|
+
if (!sourceFile) continue;
|
|
746
|
+
const serializerPath = sourceFile
|
|
747
|
+
.replace('/interfaces/', '/serializers/')
|
|
748
|
+
.replace(/\.interface\.ts$/, '.serializer.ts');
|
|
749
|
+
const relPath = relativeImport(resourcePath, serializerPath);
|
|
750
|
+
const existing = serializerImportsByPath.get(relPath) ?? [];
|
|
751
|
+
existing.push(`deserialize${name}`);
|
|
752
|
+
serializerImportsByPath.set(relPath, existing);
|
|
753
|
+
}
|
|
541
754
|
|
|
542
755
|
const importedSerializers = new Set<string>();
|
|
543
756
|
for (const name of requestModels) {
|
|
544
757
|
const resolved = resolveInterfaceName(name, ctx);
|
|
545
758
|
if (importedSerializers.has(resolved)) continue;
|
|
546
759
|
importedSerializers.add(resolved);
|
|
760
|
+
|
|
761
|
+
// If `bodyArgExpr` will fall through to passing `payload` directly
|
|
762
|
+
// (because the live SDK has the serializer file but no `serialize${T}`
|
|
763
|
+
// function), don't generate an import for a function that doesn't exist.
|
|
764
|
+
const ser = `serialize${resolved}`;
|
|
765
|
+
if (!liveSurfaceHasFunction(ser)) {
|
|
766
|
+
const sourceFile = (ctx.apiSurface?.interfaces?.[resolved] as { sourceFile?: string } | undefined)?.sourceFile;
|
|
767
|
+
const candidate = sourceFile
|
|
768
|
+
? sourceFile.replace('/interfaces/', '/serializers/').replace('.interface.ts', '.serializer.ts')
|
|
769
|
+
: `src/${resolveDir(modelToService.get(name))}/serializers/${fileName(name)}.serializer.ts`;
|
|
770
|
+
if (liveSurfaceHasFile(candidate) && !liveSurfaceHasAutogenFile(candidate)) continue;
|
|
771
|
+
}
|
|
772
|
+
|
|
547
773
|
const modelDir = modelToService.get(name);
|
|
548
774
|
const modelServiceDir = resolveDir(modelDir);
|
|
549
775
|
const relPath =
|
|
@@ -551,7 +777,7 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
551
777
|
? `./serializers/${fileName(name)}.serializer`
|
|
552
778
|
: `../${modelServiceDir}/serializers/${fileName(name)}.serializer`;
|
|
553
779
|
const existing = serializerImportsByPath.get(relPath) ?? [];
|
|
554
|
-
existing.push(
|
|
780
|
+
existing.push(ser);
|
|
555
781
|
serializerImportsByPath.set(relPath, existing);
|
|
556
782
|
}
|
|
557
783
|
|
|
@@ -568,10 +794,11 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
568
794
|
// Only import enums that actually exist in the spec's global enums list —
|
|
569
795
|
// inline string unions may have kind 'enum' but no corresponding file.
|
|
570
796
|
if (paramEnums.size > 0) {
|
|
571
|
-
const enumToService = assignEnumsToServices(ctx.spec.enums, ctx.spec.services);
|
|
797
|
+
const enumToService = assignEnumsToServices(ctx.spec.enums, ctx.spec.services, ctx.spec.models, ctx);
|
|
572
798
|
for (const name of paramEnums) {
|
|
573
799
|
if (allModels.has(name)) continue; // Already imported as a model
|
|
574
800
|
if (!specEnumNames.has(name)) continue; // No file generated for this enum
|
|
801
|
+
if (isInlineEnum(name)) continue; // Inlined at usage sites — no import needed
|
|
575
802
|
const enumDir = enumToService.get(name);
|
|
576
803
|
const enumServiceDir = resolveDir(enumDir);
|
|
577
804
|
const relPath =
|
|
@@ -700,7 +927,8 @@ function renderMethod(
|
|
|
700
927
|
// Prefer the overlay (existing method signature) if available;
|
|
701
928
|
// otherwise compute from what the render path will actually include.
|
|
702
929
|
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
703
|
-
const
|
|
930
|
+
const baselineClassMethod = baselineMethodFor(service, method, ctx);
|
|
931
|
+
const overlayMethod = ctx.overlayLookup?.methodByOperation?.get(httpKey) ?? baselineClassMethod;
|
|
704
932
|
let validParamNames: Set<string> | null = null;
|
|
705
933
|
if (overlayMethod) {
|
|
706
934
|
validParamNames = new Set(overlayMethod.params.map((p) => p.name));
|
|
@@ -886,8 +1114,11 @@ function renderMethod(
|
|
|
886
1114
|
// When an overlay method exists, prefer its return type so the JSDoc
|
|
887
1115
|
// matches the actual TypeScript signature (the overlay may use a
|
|
888
1116
|
// different model name than the OpenAPI schema).
|
|
889
|
-
|
|
890
|
-
|
|
1117
|
+
const documentedReturnType =
|
|
1118
|
+
preferredBaselineReturnType(ctx, baselineClassMethod?.returnType) ??
|
|
1119
|
+
preferredBaselineReturnType(ctx, overlayMethod?.returnType);
|
|
1120
|
+
if (documentedReturnType) {
|
|
1121
|
+
docParts.push(`@returns {${documentedReturnType}}`);
|
|
891
1122
|
} else if (plan.isPaginated && op.pagination?.itemType.kind === 'model') {
|
|
892
1123
|
// Unwrap list wrapper models to match the actual return type — the method returns
|
|
893
1124
|
// AutoPaginatable<ItemType>, not the list wrapper.
|
|
@@ -934,6 +1165,10 @@ function renderMethod(
|
|
|
934
1165
|
}
|
|
935
1166
|
}
|
|
936
1167
|
|
|
1168
|
+
if (renderOptionsObjectMethod(lines, op, plan, method, service, ctx, modelMap, specEnumNames, baselineClassMethod)) {
|
|
1169
|
+
return lines;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
937
1172
|
const preDecisionCount = lines.length;
|
|
938
1173
|
|
|
939
1174
|
if (plan.isPaginated && op.pagination && op.httpMethod === 'get') {
|
|
@@ -992,6 +1227,184 @@ function renderMethod(
|
|
|
992
1227
|
return lines;
|
|
993
1228
|
}
|
|
994
1229
|
|
|
1230
|
+
function renderOptionsObjectMethod(
|
|
1231
|
+
lines: string[],
|
|
1232
|
+
op: Operation,
|
|
1233
|
+
plan: OperationPlan,
|
|
1234
|
+
method: string,
|
|
1235
|
+
service: Service,
|
|
1236
|
+
ctx: EmitterContext,
|
|
1237
|
+
modelMap: Map<string, Model>,
|
|
1238
|
+
specEnumNames: Set<string> | undefined,
|
|
1239
|
+
baselineMethod: BaselineMethod | undefined,
|
|
1240
|
+
): boolean {
|
|
1241
|
+
const optionParam = optionsObjectParam(baselineMethod);
|
|
1242
|
+
if (!optionParam) return false;
|
|
1243
|
+
|
|
1244
|
+
const responseModel = plan.responseModelName ? resolveInterfaceName(plan.responseModelName, ctx) : null;
|
|
1245
|
+
const pathBindings = buildOptionsObjectPathBindings(op, optionParam.type, ctx);
|
|
1246
|
+
const pathStr = buildPathStr(op, buildOptionsObjectPathParamMap(op, optionParam.type, ctx));
|
|
1247
|
+
|
|
1248
|
+
if (plan.isPaginated && op.pagination && op.httpMethod === 'get') {
|
|
1249
|
+
let itemRawName = op.pagination.itemType.kind === 'model' ? op.pagination.itemType.name : null;
|
|
1250
|
+
if (itemRawName) {
|
|
1251
|
+
const pModel = modelMap.get(itemRawName);
|
|
1252
|
+
if (pModel) {
|
|
1253
|
+
const unwrapped = unwrapListModel(pModel, modelMap);
|
|
1254
|
+
if (unwrapped) itemRawName = unwrapped.name;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
const itemType =
|
|
1258
|
+
preferredBaselineTypeName(ctx, autoPaginatableItemType(baselineMethod?.returnType)) ??
|
|
1259
|
+
(itemRawName ? resolveInterfaceName(itemRawName, ctx) : responseModel);
|
|
1260
|
+
if (!itemType) return false;
|
|
1261
|
+
const wireType = wireInterfaceName(itemType);
|
|
1262
|
+
const returnType =
|
|
1263
|
+
preferredBaselineReturnType(ctx, baselineMethod?.returnType) ?? `Promise<AutoPaginatable<${itemType}>>`;
|
|
1264
|
+
lines.push(` async ${method}(options: ${optionParam.type}): ${returnType} {`);
|
|
1265
|
+
renderOptionsObjectDestructure(lines, pathBindings, 'paginationOptions');
|
|
1266
|
+
lines.push(` return new AutoPaginatable(`);
|
|
1267
|
+
lines.push(` await fetchAndDeserialize<${wireType}, ${itemType}>(`);
|
|
1268
|
+
lines.push(` this.workos,`);
|
|
1269
|
+
lines.push(` ${pathStr},`);
|
|
1270
|
+
lines.push(` deserialize${itemType},`);
|
|
1271
|
+
lines.push(` paginationOptions,`);
|
|
1272
|
+
lines.push(` ),`);
|
|
1273
|
+
lines.push(` (params) =>`);
|
|
1274
|
+
lines.push(` fetchAndDeserialize<${wireType}, ${itemType}>(`);
|
|
1275
|
+
lines.push(` this.workos,`);
|
|
1276
|
+
lines.push(` ${pathStr},`);
|
|
1277
|
+
lines.push(` deserialize${itemType},`);
|
|
1278
|
+
lines.push(` params,`);
|
|
1279
|
+
lines.push(` ),`);
|
|
1280
|
+
lines.push(` paginationOptions,`);
|
|
1281
|
+
lines.push(` );`);
|
|
1282
|
+
lines.push(' }');
|
|
1283
|
+
return true;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
if (plan.isDelete && !plan.hasBody) {
|
|
1287
|
+
lines.push(` async ${method}(options: ${optionParam.type}): Promise<void> {`);
|
|
1288
|
+
renderOptionsObjectDestructure(lines, pathBindings);
|
|
1289
|
+
lines.push(` await this.workos.delete(${pathStr});`);
|
|
1290
|
+
lines.push(' }');
|
|
1291
|
+
return true;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
if (plan.hasBody) {
|
|
1295
|
+
const bodyInfo = extractRequestBodyType(op, ctx);
|
|
1296
|
+
let requestType: string;
|
|
1297
|
+
let bodyExpr: string;
|
|
1298
|
+
let entityType: string;
|
|
1299
|
+
if (bodyInfo?.kind === 'model') {
|
|
1300
|
+
requestType = resolveInterfaceName(bodyInfo.name, ctx);
|
|
1301
|
+
bodyExpr = bodyArgExpr(bodyInfo.name, requestType, ctx, 'payload');
|
|
1302
|
+
entityType = requestEntityType(bodyExpr, requestType);
|
|
1303
|
+
} else if (bodyInfo?.kind === 'union') {
|
|
1304
|
+
requestType = bodyInfo.typeStr;
|
|
1305
|
+
bodyExpr = bodyInfo.discriminator
|
|
1306
|
+
? renderUnionBodySerializer(bodyInfo.discriminator, ctx)
|
|
1307
|
+
: renderNonDiscriminatedUnionBodySerializer(bodyInfo.modelNames, ctx);
|
|
1308
|
+
entityType = unionEntityType(bodyInfo.modelNames, ctx);
|
|
1309
|
+
} else {
|
|
1310
|
+
requestType = 'Record<string, unknown>';
|
|
1311
|
+
bodyExpr = 'payload';
|
|
1312
|
+
entityType = requestType;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
if (plan.isDelete) {
|
|
1316
|
+
lines.push(` async ${method}(options: ${optionParam.type}): Promise<void> {`);
|
|
1317
|
+
renderOptionsObjectDestructure(lines, pathBindings, 'payload');
|
|
1318
|
+
lines.push(` await this.workos.deleteWithBody<${entityType}>(${pathStr}, ${bodyExpr});`);
|
|
1319
|
+
lines.push(' }');
|
|
1320
|
+
return true;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (!responseModel) {
|
|
1324
|
+
lines.push(` async ${method}(options: ${optionParam.type}): Promise<void> {`);
|
|
1325
|
+
renderOptionsObjectDestructure(lines, pathBindings, 'payload');
|
|
1326
|
+
lines.push(` await this.workos.${op.httpMethod}<void, ${entityType}>(${pathStr}, ${bodyExpr});`);
|
|
1327
|
+
lines.push(' }');
|
|
1328
|
+
return true;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
const returnType = plan.isArrayResponse ? `${responseModel}[]` : responseModel;
|
|
1332
|
+
const wireType = plan.isArrayResponse ? `${wireInterfaceName(responseModel)}[]` : wireInterfaceName(responseModel);
|
|
1333
|
+
const returnExpr = plan.isArrayResponse
|
|
1334
|
+
? `data.map(deserialize${responseModel})`
|
|
1335
|
+
: `deserialize${responseModel}(data)`;
|
|
1336
|
+
|
|
1337
|
+
lines.push(` async ${method}(options: ${optionParam.type}): Promise<${returnType}> {`);
|
|
1338
|
+
renderOptionsObjectDestructure(lines, pathBindings, 'payload');
|
|
1339
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}, ${entityType}>(`);
|
|
1340
|
+
lines.push(` ${pathStr},`);
|
|
1341
|
+
lines.push(` ${bodyExpr},`);
|
|
1342
|
+
lines.push(' );');
|
|
1343
|
+
lines.push(` return ${returnExpr};`);
|
|
1344
|
+
lines.push(' }');
|
|
1345
|
+
return true;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (responseModel) {
|
|
1349
|
+
const returnType = plan.isArrayResponse ? `${responseModel}[]` : responseModel;
|
|
1350
|
+
const wireType = plan.isArrayResponse ? `${wireInterfaceName(responseModel)}[]` : wireInterfaceName(responseModel);
|
|
1351
|
+
const returnExpr = plan.isArrayResponse
|
|
1352
|
+
? `data.map(deserialize${responseModel})`
|
|
1353
|
+
: `deserialize${responseModel}(data)`;
|
|
1354
|
+
|
|
1355
|
+
lines.push(` async ${method}(options: ${optionParam.type}): Promise<${returnType}> {`);
|
|
1356
|
+
renderOptionsObjectDestructure(lines, pathBindings);
|
|
1357
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr});`);
|
|
1358
|
+
lines.push(` return ${returnExpr};`);
|
|
1359
|
+
lines.push(' }');
|
|
1360
|
+
return true;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
return false;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function renderOptionsObjectDestructure(lines: string[], pathBindings: string[], restName?: string): void {
|
|
1367
|
+
if (pathBindings.length > 0 && restName) {
|
|
1368
|
+
lines.push(` const { ${pathBindings.join(', ')}, ...${restName} } = options;`);
|
|
1369
|
+
} else if (pathBindings.length > 0) {
|
|
1370
|
+
lines.push(` const { ${pathBindings.join(', ')} } = options;`);
|
|
1371
|
+
} else if (restName) {
|
|
1372
|
+
lines.push(` const ${restName} = options;`);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function buildOptionsObjectPathBindings(op: Operation, optionType: string, ctx: EmitterContext): string[] {
|
|
1377
|
+
// Return resolved SDK field names directly — the URL template uses these
|
|
1378
|
+
// names too (via the param-name map threaded into buildNodePathExpression),
|
|
1379
|
+
// so the destructure no longer needs `optionField: localName` renames.
|
|
1380
|
+
return op.pathParams.map((param) => resolveOptionsObjectField(fieldName(param.name), optionType, ctx));
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* Map spec path-param names (e.g. `omId`) to the SDK field name exposed on
|
|
1385
|
+
* the options interface (e.g. `organizationMembershipId`). Empty when every
|
|
1386
|
+
* path param's spec name already matches the SDK field. Consumed by
|
|
1387
|
+
* `buildNodePathExpression` so the URL template binds to the same identifier
|
|
1388
|
+
* the destructure does.
|
|
1389
|
+
*/
|
|
1390
|
+
function buildOptionsObjectPathParamMap(op: Operation, optionType: string, ctx: EmitterContext): Map<string, string> {
|
|
1391
|
+
const map = new Map<string, string>();
|
|
1392
|
+
for (const param of op.pathParams) {
|
|
1393
|
+
const localName = fieldName(param.name);
|
|
1394
|
+
const sdkField = resolveOptionsObjectField(localName, optionType, ctx);
|
|
1395
|
+
if (sdkField !== localName) map.set(param.name, sdkField);
|
|
1396
|
+
}
|
|
1397
|
+
return map;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function resolveOptionsObjectField(localName: string, optionType: string, ctx: EmitterContext): string {
|
|
1401
|
+
const fields = ctx.apiSurface?.interfaces?.[optionType]?.fields;
|
|
1402
|
+
if (!fields) return localName;
|
|
1403
|
+
if (fields[localName]) return localName;
|
|
1404
|
+
if (localName === 'omId' && fields.organizationMembershipId) return 'organizationMembershipId';
|
|
1405
|
+
return localName;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
995
1408
|
function renderPaginatedMethod(
|
|
996
1409
|
lines: string[],
|
|
997
1410
|
op: Operation,
|
|
@@ -1062,9 +1475,11 @@ function renderDeleteWithBodyMethod(
|
|
|
1062
1475
|
const bodyInfo = extractRequestBodyType(op, ctx);
|
|
1063
1476
|
let requestType: string;
|
|
1064
1477
|
let bodyExpr: string;
|
|
1478
|
+
let entityType: string;
|
|
1065
1479
|
if (bodyInfo?.kind === 'model') {
|
|
1066
1480
|
requestType = resolveInterfaceName(bodyInfo.name, ctx);
|
|
1067
|
-
bodyExpr =
|
|
1481
|
+
bodyExpr = bodyArgExpr(bodyInfo.name, requestType, ctx);
|
|
1482
|
+
entityType = requestEntityType(bodyExpr, requestType);
|
|
1068
1483
|
} else if (bodyInfo?.kind === 'union') {
|
|
1069
1484
|
requestType = bodyInfo.typeStr;
|
|
1070
1485
|
if (bodyInfo.discriminator) {
|
|
@@ -1072,9 +1487,11 @@ function renderDeleteWithBodyMethod(
|
|
|
1072
1487
|
} else {
|
|
1073
1488
|
bodyExpr = renderNonDiscriminatedUnionBodySerializer(bodyInfo.modelNames, ctx);
|
|
1074
1489
|
}
|
|
1490
|
+
entityType = unionEntityType(bodyInfo.modelNames, ctx);
|
|
1075
1491
|
} else {
|
|
1076
1492
|
requestType = 'Record<string, unknown>';
|
|
1077
1493
|
bodyExpr = 'payload';
|
|
1494
|
+
entityType = requestType;
|
|
1078
1495
|
}
|
|
1079
1496
|
|
|
1080
1497
|
const paramParts: string[] = [];
|
|
@@ -1086,7 +1503,7 @@ function renderDeleteWithBodyMethod(
|
|
|
1086
1503
|
paramParts.push(`payload: ${requestType}`);
|
|
1087
1504
|
|
|
1088
1505
|
lines.push(` async ${method}(${paramParts.join(', ')}): Promise<void> {`);
|
|
1089
|
-
lines.push(` await this.workos.deleteWithBody(${pathStr}, ${bodyExpr});`);
|
|
1506
|
+
lines.push(` await this.workos.deleteWithBody<${entityType}>(${pathStr}, ${bodyExpr});`);
|
|
1090
1507
|
lines.push(' }');
|
|
1091
1508
|
}
|
|
1092
1509
|
|
|
@@ -1103,9 +1520,11 @@ function renderBodyMethod(
|
|
|
1103
1520
|
const bodyInfo = extractRequestBodyType(op, ctx);
|
|
1104
1521
|
let requestType: string;
|
|
1105
1522
|
let bodyExpr: string;
|
|
1523
|
+
let entityType: string;
|
|
1106
1524
|
if (bodyInfo?.kind === 'model') {
|
|
1107
1525
|
requestType = resolveInterfaceName(bodyInfo.name, ctx);
|
|
1108
|
-
bodyExpr =
|
|
1526
|
+
bodyExpr = bodyArgExpr(bodyInfo.name, requestType, ctx);
|
|
1527
|
+
entityType = requestEntityType(bodyExpr, requestType);
|
|
1109
1528
|
} else if (bodyInfo?.kind === 'union') {
|
|
1110
1529
|
requestType = bodyInfo.typeStr;
|
|
1111
1530
|
if (bodyInfo.discriminator) {
|
|
@@ -1113,9 +1532,11 @@ function renderBodyMethod(
|
|
|
1113
1532
|
} else {
|
|
1114
1533
|
bodyExpr = renderNonDiscriminatedUnionBodySerializer(bodyInfo.modelNames, ctx);
|
|
1115
1534
|
}
|
|
1535
|
+
entityType = unionEntityType(bodyInfo.modelNames, ctx);
|
|
1116
1536
|
} else {
|
|
1117
1537
|
requestType = 'Record<string, unknown>';
|
|
1118
1538
|
bodyExpr = 'payload';
|
|
1539
|
+
entityType = requestType;
|
|
1119
1540
|
}
|
|
1120
1541
|
|
|
1121
1542
|
const paramParts: string[] = [];
|
|
@@ -1149,13 +1570,13 @@ function renderBodyMethod(
|
|
|
1149
1570
|
lines.push(` async ${method}(${paramsStr}): Promise<${returnType}> {`);
|
|
1150
1571
|
if (plan.isIdempotentPost) {
|
|
1151
1572
|
if (hasCustomEncoding) {
|
|
1152
|
-
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(`);
|
|
1573
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}, ${entityType}>(`);
|
|
1153
1574
|
lines.push(` ${pathStr},`);
|
|
1154
1575
|
lines.push(` ${bodyExpr},`);
|
|
1155
1576
|
lines.push(` { ...requestOptions${encodingOption} },`);
|
|
1156
1577
|
lines.push(' );');
|
|
1157
1578
|
} else {
|
|
1158
|
-
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(`);
|
|
1579
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}, ${entityType}>(`);
|
|
1159
1580
|
lines.push(` ${pathStr},`);
|
|
1160
1581
|
lines.push(` ${bodyExpr},`);
|
|
1161
1582
|
lines.push(' requestOptions,');
|
|
@@ -1163,13 +1584,13 @@ function renderBodyMethod(
|
|
|
1163
1584
|
}
|
|
1164
1585
|
} else {
|
|
1165
1586
|
if (hasCustomEncoding) {
|
|
1166
|
-
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(`);
|
|
1587
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}, ${entityType}>(`);
|
|
1167
1588
|
lines.push(` ${pathStr},`);
|
|
1168
1589
|
lines.push(` ${bodyExpr},`);
|
|
1169
1590
|
lines.push(` { ${encodingOption.slice(2)} },`);
|
|
1170
1591
|
lines.push(' );');
|
|
1171
1592
|
} else {
|
|
1172
|
-
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(`);
|
|
1593
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}, ${entityType}>(`);
|
|
1173
1594
|
lines.push(` ${pathStr},`);
|
|
1174
1595
|
lines.push(` ${bodyExpr},`);
|
|
1175
1596
|
lines.push(' );');
|
|
@@ -1316,12 +1737,14 @@ function renderVoidMethod(
|
|
|
1316
1737
|
|
|
1317
1738
|
let bodyParam = '';
|
|
1318
1739
|
let bodyExpr = 'payload';
|
|
1740
|
+
let entityType = 'Record<string, unknown>';
|
|
1319
1741
|
if (plan.hasBody) {
|
|
1320
1742
|
const bodyInfo = extractRequestBodyType(op, ctx);
|
|
1321
1743
|
if (bodyInfo?.kind === 'model') {
|
|
1322
1744
|
const requestType = resolveInterfaceName(bodyInfo.name, ctx);
|
|
1323
1745
|
bodyParam = `payload: ${requestType}`;
|
|
1324
|
-
bodyExpr =
|
|
1746
|
+
bodyExpr = bodyArgExpr(bodyInfo.name, requestType, ctx);
|
|
1747
|
+
entityType = requestEntityType(bodyExpr, requestType);
|
|
1325
1748
|
} else if (bodyInfo?.kind === 'union') {
|
|
1326
1749
|
bodyParam = `payload: ${bodyInfo.typeStr}`;
|
|
1327
1750
|
if (bodyInfo.discriminator) {
|
|
@@ -1329,9 +1752,11 @@ function renderVoidMethod(
|
|
|
1329
1752
|
} else {
|
|
1330
1753
|
bodyExpr = renderNonDiscriminatedUnionBodySerializer(bodyInfo.modelNames, ctx);
|
|
1331
1754
|
}
|
|
1755
|
+
entityType = unionEntityType(bodyInfo.modelNames, ctx);
|
|
1332
1756
|
} else {
|
|
1333
1757
|
bodyParam = 'payload: Record<string, unknown>';
|
|
1334
1758
|
bodyExpr = 'payload';
|
|
1759
|
+
entityType = 'Record<string, unknown>';
|
|
1335
1760
|
}
|
|
1336
1761
|
}
|
|
1337
1762
|
|
|
@@ -1343,7 +1768,7 @@ function renderVoidMethod(
|
|
|
1343
1768
|
|
|
1344
1769
|
lines.push(` async ${method}(${allParams}): Promise<void> {`);
|
|
1345
1770
|
if (plan.hasBody) {
|
|
1346
|
-
lines.push(` await this.workos.${op.httpMethod}(${pathStr}, ${bodyExpr});`);
|
|
1771
|
+
lines.push(` await this.workos.${op.httpMethod}<void, ${entityType}>(${pathStr}, ${bodyExpr});`);
|
|
1347
1772
|
} else if (hasQuery) {
|
|
1348
1773
|
if (hasInjected) {
|
|
1349
1774
|
// Build query object with visible params, defaults, and inferred fields
|
|
@@ -1417,8 +1842,8 @@ function renderQueryExpr(queryParams: { name: string; required: boolean }[]): st
|
|
|
1417
1842
|
return `options ? { ${parts.join(', ')} } : undefined`;
|
|
1418
1843
|
}
|
|
1419
1844
|
|
|
1420
|
-
function buildPathStr(op: Operation): string {
|
|
1421
|
-
return buildNodePathExpression(op.path);
|
|
1845
|
+
function buildPathStr(op: Operation, paramNameMap?: Map<string, string>): string {
|
|
1846
|
+
return buildNodePathExpression(op.path, paramNameMap);
|
|
1422
1847
|
}
|
|
1423
1848
|
|
|
1424
1849
|
function buildPathParams(op: Operation, specEnumNames?: Set<string>): string {
|