@workos/oagen-emitters 0.2.1 → 0.4.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/.husky/pre-commit +1 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +15 -0
- package/README.md +129 -0
- package/dist/index.d.mts +13 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +14549 -3385
- package/dist/index.mjs.map +1 -1
- package/docs/sdk-architecture/dotnet.md +336 -0
- package/docs/sdk-architecture/go.md +338 -0
- package/docs/sdk-architecture/php.md +315 -0
- package/docs/sdk-architecture/python.md +511 -0
- package/oagen.config.ts +328 -2
- package/package.json +9 -5
- package/scripts/generate-php.js +13 -0
- package/scripts/git-push-with-published-oagen.sh +21 -0
- package/smoke/sdk-dotnet.ts +45 -12
- package/smoke/sdk-go.ts +116 -42
- package/smoke/sdk-php.ts +28 -26
- package/smoke/sdk-python.ts +5 -2
- 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 +246 -0
- package/src/dotnet/manifest.ts +36 -0
- package/src/dotnet/models.ts +344 -0
- package/src/dotnet/naming.ts +330 -0
- package/src/dotnet/resources.ts +622 -0
- package/src/dotnet/tests.ts +693 -0
- package/src/dotnet/type-map.ts +201 -0
- package/src/dotnet/wrappers.ts +186 -0
- package/src/go/client.ts +141 -0
- package/src/go/enums.ts +196 -0
- package/src/go/fixtures.ts +212 -0
- package/src/go/index.ts +84 -0
- package/src/go/manifest.ts +36 -0
- package/src/go/models.ts +254 -0
- package/src/go/naming.ts +179 -0
- package/src/go/resources.ts +827 -0
- package/src/go/tests.ts +751 -0
- package/src/go/type-map.ts +82 -0
- package/src/go/wrappers.ts +261 -0
- package/src/index.ts +4 -0
- package/src/kotlin/client.ts +53 -0
- package/src/kotlin/enums.ts +162 -0
- package/src/kotlin/index.ts +92 -0
- package/src/kotlin/manifest.ts +55 -0
- package/src/kotlin/models.ts +395 -0
- package/src/kotlin/naming.ts +223 -0
- package/src/kotlin/overrides.ts +25 -0
- package/src/kotlin/resources.ts +667 -0
- package/src/kotlin/tests.ts +1019 -0
- package/src/kotlin/type-map.ts +123 -0
- package/src/kotlin/wrappers.ts +168 -0
- package/src/node/client.ts +128 -115
- package/src/node/enums.ts +9 -0
- package/src/node/errors.ts +37 -232
- package/src/node/field-plan.ts +726 -0
- package/src/node/fixtures.ts +9 -1
- package/src/node/index.ts +3 -9
- package/src/node/models.ts +178 -21
- package/src/node/naming.ts +49 -111
- package/src/node/resources.ts +527 -397
- package/src/node/sdk-errors.ts +41 -0
- package/src/node/tests.ts +69 -19
- package/src/node/type-map.ts +4 -2
- package/src/node/utils.ts +13 -71
- package/src/node/wrappers.ts +151 -0
- package/src/php/client.ts +179 -0
- package/src/php/enums.ts +67 -0
- package/src/php/errors.ts +9 -0
- package/src/php/fixtures.ts +181 -0
- package/src/php/index.ts +96 -0
- package/src/php/manifest.ts +36 -0
- package/src/php/models.ts +310 -0
- package/src/php/naming.ts +279 -0
- package/src/php/resources.ts +636 -0
- package/src/php/tests.ts +609 -0
- package/src/php/type-map.ts +90 -0
- package/src/php/utils.ts +18 -0
- package/src/php/wrappers.ts +152 -0
- package/src/python/client.ts +345 -0
- package/src/python/enums.ts +313 -0
- package/src/python/fixtures.ts +196 -0
- package/src/python/index.ts +95 -0
- package/src/python/manifest.ts +38 -0
- package/src/python/models.ts +688 -0
- package/src/python/naming.ts +189 -0
- package/src/python/resources.ts +1322 -0
- package/src/python/tests.ts +1335 -0
- package/src/python/type-map.ts +93 -0
- package/src/python/wrappers.ts +191 -0
- package/src/shared/model-utils.ts +472 -0
- package/src/shared/naming-utils.ts +154 -0
- package/src/shared/non-spec-services.ts +54 -0
- package/src/shared/resolved-ops.ts +109 -0
- package/src/shared/wrapper-utils.ts +70 -0
- 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 +260 -0
- package/test/dotnet/resources.test.ts +255 -0
- package/test/dotnet/tests.test.ts +202 -0
- package/test/go/client.test.ts +92 -0
- package/test/go/enums.test.ts +132 -0
- package/test/go/errors.test.ts +9 -0
- package/test/go/models.test.ts +265 -0
- package/test/go/resources.test.ts +408 -0
- package/test/go/tests.test.ts +143 -0
- package/test/kotlin/models.test.ts +135 -0
- package/test/kotlin/tests.test.ts +176 -0
- package/test/node/client.test.ts +92 -12
- package/test/node/enums.test.ts +2 -0
- package/test/node/errors.test.ts +2 -41
- package/test/node/models.test.ts +2 -0
- package/test/node/naming.test.ts +23 -0
- package/test/node/resources.test.ts +315 -84
- package/test/node/serializers.test.ts +3 -1
- package/test/node/type-map.test.ts +11 -0
- package/test/php/client.test.ts +95 -0
- package/test/php/enums.test.ts +173 -0
- package/test/php/errors.test.ts +9 -0
- package/test/php/models.test.ts +497 -0
- package/test/php/resources.test.ts +682 -0
- package/test/php/tests.test.ts +185 -0
- package/test/python/client.test.ts +200 -0
- package/test/python/enums.test.ts +228 -0
- package/test/python/errors.test.ts +16 -0
- package/test/python/manifest.test.ts +74 -0
- package/test/python/models.test.ts +716 -0
- package/test/python/resources.test.ts +617 -0
- package/test/python/tests.test.ts +202 -0
- package/src/node/common.ts +0 -273
- package/src/node/config.ts +0 -71
- package/src/node/serializers.ts +0 -746
package/src/node/resources.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
// @oagen-ignore: Operation.async — all TypeScript SDK methods are async by nature
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
Service,
|
|
5
|
+
Operation,
|
|
6
|
+
EmitterContext,
|
|
7
|
+
GeneratedFile,
|
|
8
|
+
TypeRef,
|
|
9
|
+
Model,
|
|
10
|
+
ResolvedOperation,
|
|
11
|
+
} from '@workos/oagen';
|
|
4
12
|
import { planOperation, toPascalCase, toCamelCase } from '@workos/oagen';
|
|
5
13
|
import type { OperationPlan } from '@workos/oagen';
|
|
6
14
|
import { mapTypeRef } from './type-map.js';
|
|
@@ -23,6 +31,16 @@ import {
|
|
|
23
31
|
} from './utils.js';
|
|
24
32
|
import { assignEnumsToServices } from './enums.js';
|
|
25
33
|
import { unwrapListModel } from './fixtures.js';
|
|
34
|
+
import { buildNodeStatusExceptions } from './sdk-errors.js';
|
|
35
|
+
import {
|
|
36
|
+
buildResolvedLookup,
|
|
37
|
+
lookupResolved,
|
|
38
|
+
groupByMount,
|
|
39
|
+
getOpDefaults,
|
|
40
|
+
getOpInferFromClient,
|
|
41
|
+
} from '../shared/resolved-ops.js';
|
|
42
|
+
import { generateWrapperMethods, collectWrapperResponseModels } from './wrappers.js';
|
|
43
|
+
import { resolveWrapperParams } from '../shared/wrapper-utils.js';
|
|
26
44
|
|
|
27
45
|
/**
|
|
28
46
|
* Check whether the baseline (hand-written) class has a constructor compatible
|
|
@@ -63,15 +81,9 @@ export function resolveResourceClassName(service: Service, ctx: EmitterContext):
|
|
|
63
81
|
/** Standard pagination query params handled by PaginationOptions — not imported individually. */
|
|
64
82
|
const PAGINATION_PARAM_NAMES = new Set(['limit', 'before', 'after', 'order']);
|
|
65
83
|
|
|
66
|
-
/** Map HTTP status codes to their corresponding exception class names for @throws docs.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
401: 'UnauthorizedException',
|
|
70
|
-
404: 'NotFoundException',
|
|
71
|
-
409: 'ConflictException',
|
|
72
|
-
422: 'UnprocessableEntityException',
|
|
73
|
-
429: 'RateLimitExceededException',
|
|
74
|
-
};
|
|
84
|
+
/** Map HTTP status codes to their corresponding exception class names for @throws docs.
|
|
85
|
+
* Built from sdk.errors.statusCodeMap with Node-specific naming overrides. */
|
|
86
|
+
const STATUS_TO_EXCEPTION_NAME: Record<number, string> = buildNodeStatusExceptions();
|
|
75
87
|
|
|
76
88
|
/**
|
|
77
89
|
* Compute the options interface name for a paginated method.
|
|
@@ -92,18 +104,9 @@ function httpMethodNeedsBody(method: string): boolean {
|
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
// ---------------------------------------------------------------------------
|
|
95
|
-
// Method-name
|
|
107
|
+
// Method-name deduplication helpers
|
|
96
108
|
// ---------------------------------------------------------------------------
|
|
97
109
|
|
|
98
|
-
/** Map HTTP methods to expected CRUD verb prefixes for method name matching. */
|
|
99
|
-
const HTTP_VERB_PREFIXES: Record<string, string[]> = {
|
|
100
|
-
get: ['list', 'get', 'fetch', 'retrieve', 'find'],
|
|
101
|
-
post: ['create', 'add', 'insert', 'send'],
|
|
102
|
-
put: ['set', 'update', 'replace', 'put'],
|
|
103
|
-
patch: ['update', 'patch', 'modify'],
|
|
104
|
-
delete: ['delete', 'remove', 'revoke'],
|
|
105
|
-
};
|
|
106
|
-
|
|
107
110
|
/** Split a camelCase/PascalCase name into lowercase word parts. */
|
|
108
111
|
function splitCamelWords(name: string): string[] {
|
|
109
112
|
const parts: string[] = [];
|
|
@@ -123,209 +126,6 @@ function singularize(word: string): string {
|
|
|
123
126
|
return word.endsWith('s') && !word.endsWith('ss') ? word.slice(0, -1) : word;
|
|
124
127
|
}
|
|
125
128
|
|
|
126
|
-
/**
|
|
127
|
-
* Batch-reconcile generated method names against the api-surface class methods.
|
|
128
|
-
*
|
|
129
|
-
* When the overlay doesn't map an operation (missing from previous manifest or
|
|
130
|
-
* fuzzy-matching failed), the emitter falls back to the spec-derived name which
|
|
131
|
-
* often doesn't match the hand-written SDK. This function uses three passes
|
|
132
|
-
* with progressive exclusion to find the best surface match:
|
|
133
|
-
*
|
|
134
|
-
* 1. **Overlay-resolved** — already correct, mark as taken.
|
|
135
|
-
* 2. **Word-set match** — same content words regardless of order (handles
|
|
136
|
-
* `listRolesOrganizations` ↔ `listOrganizationRoles`).
|
|
137
|
-
* 3. **Path-context match** — all surface-method content words appear in the
|
|
138
|
-
* operation's URL path segments (handles `findById` ↔ `getResource`).
|
|
139
|
-
*
|
|
140
|
-
* After each pass, matched surface methods are removed from the pool so that
|
|
141
|
-
* ambiguous cases (e.g., `listEnvironmentRoles` vs `listOrganizationRoles`)
|
|
142
|
-
* resolve by elimination.
|
|
143
|
-
*/
|
|
144
|
-
function reconcileMethodNames(
|
|
145
|
-
plans: Array<{ op: Operation; plan: OperationPlan; method: string }>,
|
|
146
|
-
service: Service,
|
|
147
|
-
ctx: EmitterContext,
|
|
148
|
-
): void {
|
|
149
|
-
const className = resolveResourceClassName(service, ctx);
|
|
150
|
-
const classMethods = ctx.apiSurface?.classes?.[className]?.methods;
|
|
151
|
-
if (!classMethods) return;
|
|
152
|
-
|
|
153
|
-
const available = new Set(Object.keys(classMethods));
|
|
154
|
-
const resolved = new Map<(typeof plans)[number], string>();
|
|
155
|
-
|
|
156
|
-
// Exclude surface methods that are overlay-mapped by OTHER services'
|
|
157
|
-
// operations. This prevents the reconciliation from stealing methods
|
|
158
|
-
// that belong to a different service (e.g., `createPermission` belongs
|
|
159
|
-
// to the Permissions service, not the Authorization service).
|
|
160
|
-
const thisServicePaths = new Set(service.operations.map((op) => op.path));
|
|
161
|
-
if (ctx.overlayLookup?.methodByOperation) {
|
|
162
|
-
for (const [httpKey, info] of ctx.overlayLookup.methodByOperation) {
|
|
163
|
-
if (info.className !== className) continue; // different class
|
|
164
|
-
const path = httpKey.split(' ')[1];
|
|
165
|
-
if (thisServicePaths.has(path)) continue; // same service
|
|
166
|
-
// This method is mapped to an operation in a different service
|
|
167
|
-
available.delete(info.methodName);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Determine which plans are already overlay-resolved
|
|
172
|
-
const overlayResolved = new Set<(typeof plans)[number]>();
|
|
173
|
-
for (const plan of plans) {
|
|
174
|
-
const httpKey = `${plan.op.httpMethod.toUpperCase()} ${plan.op.path}`;
|
|
175
|
-
if (ctx.overlayLookup?.methodByOperation?.get(httpKey)) {
|
|
176
|
-
overlayResolved.add(plan);
|
|
177
|
-
if (available.has(plan.method)) {
|
|
178
|
-
resolved.set(plan, plan.method);
|
|
179
|
-
available.delete(plan.method);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Helper: check verb compatibility.
|
|
185
|
-
// When specVerb is provided, require the surface method to use the same verb
|
|
186
|
-
// subgroup (e.g., "list" operations only match "list" methods, not "get").
|
|
187
|
-
const verbMatches = (methodName: string, httpMethod: string, specVerb?: string): boolean => {
|
|
188
|
-
const prefixes = HTTP_VERB_PREFIXES[httpMethod] ?? [];
|
|
189
|
-
const lower = methodName.toLowerCase();
|
|
190
|
-
if (!prefixes.some((p) => lower.startsWith(p))) return false;
|
|
191
|
-
if (specVerb) {
|
|
192
|
-
const surfaceVerb = splitCamelWords(methodName)[0];
|
|
193
|
-
// "list" is a distinct verb subgroup from "get/find/fetch/retrieve"
|
|
194
|
-
if (specVerb === 'list' && surfaceVerb !== 'list') return false;
|
|
195
|
-
if (specVerb !== 'list' && surfaceVerb === 'list') return false;
|
|
196
|
-
}
|
|
197
|
-
return true;
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
// Pass 1: Word-set matching (handles word-order differences)
|
|
201
|
-
for (const plan of plans) {
|
|
202
|
-
if (resolved.has(plan)) continue;
|
|
203
|
-
const specVerb = splitCamelWords(plan.method)[0];
|
|
204
|
-
const specWords = splitCamelWords(plan.method).slice(1).map(singularize); // skip verb
|
|
205
|
-
const specSet = new Set(specWords);
|
|
206
|
-
if (specSet.size === 0) continue;
|
|
207
|
-
|
|
208
|
-
let match: string | null = null;
|
|
209
|
-
for (const name of available) {
|
|
210
|
-
if (!verbMatches(name, plan.op.httpMethod, specVerb)) continue;
|
|
211
|
-
const methodWords = splitCamelWords(name).slice(1).map(singularize);
|
|
212
|
-
if (methodWords.length !== specWords.length) continue;
|
|
213
|
-
const methodSet = new Set(methodWords);
|
|
214
|
-
if (specSet.size === methodSet.size && [...specSet].every((w) => methodSet.has(w))) {
|
|
215
|
-
if (match !== null) {
|
|
216
|
-
match = null; // ambiguous — more than one match
|
|
217
|
-
break;
|
|
218
|
-
}
|
|
219
|
-
match = name;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
if (match) {
|
|
223
|
-
resolved.set(plan, match);
|
|
224
|
-
available.delete(match);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Pass 2: Path-context matching (handles different naming e.g., findById → getResource)
|
|
229
|
-
// To avoid false matches (e.g., `createPermission` matching a role-permission path
|
|
230
|
-
// just because "permission" appears somewhere in the URL), require that the method's
|
|
231
|
-
// content words reference the LEAF resource of the path, not just any segment.
|
|
232
|
-
for (const plan of plans) {
|
|
233
|
-
if (resolved.has(plan)) continue;
|
|
234
|
-
const specVerb = splitCamelWords(plan.method)[0];
|
|
235
|
-
const pathSegments = plan.op.path.split('/').filter((s) => s && !s.startsWith('{'));
|
|
236
|
-
const pathWords = new Set(pathSegments.flatMap((s) => s.split('_')).map(singularize));
|
|
237
|
-
// The "leaf" words come from the last non-param segment — the most specific resource.
|
|
238
|
-
let bestMatch: string | null = null;
|
|
239
|
-
let bestLen = 0;
|
|
240
|
-
let ambiguous = false;
|
|
241
|
-
for (const name of available) {
|
|
242
|
-
if (!verbMatches(name, plan.op.httpMethod, specVerb)) continue;
|
|
243
|
-
const methodWords = splitCamelWords(name).slice(1).map(singularize);
|
|
244
|
-
if (methodWords.length === 0) continue;
|
|
245
|
-
// All method content words must appear in path context
|
|
246
|
-
if (!methodWords.every((w) => pathWords.has(w))) continue;
|
|
247
|
-
// For paths with 3+ non-param segments (nested sub-resources like
|
|
248
|
-
// /authorization/roles/{slug}/permissions), require at least 2 content
|
|
249
|
-
// words. A single-word method like `createPermission` should only match
|
|
250
|
-
// a top-level path like /authorization/permissions, not a nested one.
|
|
251
|
-
if (methodWords.length < 2 && pathSegments.length > 2) continue;
|
|
252
|
-
if (methodWords.length > bestLen) {
|
|
253
|
-
bestMatch = name;
|
|
254
|
-
bestLen = methodWords.length;
|
|
255
|
-
ambiguous = false;
|
|
256
|
-
} else if (methodWords.length === bestLen) {
|
|
257
|
-
ambiguous = true;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
if (bestMatch && !ambiguous) {
|
|
261
|
-
resolved.set(plan, bestMatch);
|
|
262
|
-
available.delete(bestMatch);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Pass 3: Retry word-set and path-context for still-unresolved plans
|
|
267
|
-
// (earlier passes may have eliminated ambiguous candidates)
|
|
268
|
-
for (const plan of plans) {
|
|
269
|
-
if (resolved.has(plan)) continue;
|
|
270
|
-
const specVerb = splitCamelWords(plan.method)[0];
|
|
271
|
-
|
|
272
|
-
// Retry word-set
|
|
273
|
-
const specWords = splitCamelWords(plan.method).slice(1).map(singularize);
|
|
274
|
-
const specSet = new Set(specWords);
|
|
275
|
-
let match: string | null = null;
|
|
276
|
-
for (const name of available) {
|
|
277
|
-
if (!verbMatches(name, plan.op.httpMethod, specVerb)) continue;
|
|
278
|
-
const methodWords = splitCamelWords(name).slice(1).map(singularize);
|
|
279
|
-
if (methodWords.length !== specWords.length) continue;
|
|
280
|
-
const methodSet = new Set(methodWords);
|
|
281
|
-
if (specSet.size === methodSet.size && [...specSet].every((w) => methodSet.has(w))) {
|
|
282
|
-
if (match !== null) {
|
|
283
|
-
match = null;
|
|
284
|
-
break;
|
|
285
|
-
}
|
|
286
|
-
match = name;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
if (match) {
|
|
290
|
-
resolved.set(plan, match);
|
|
291
|
-
available.delete(match);
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Retry path-context (with leaf-word check)
|
|
296
|
-
const pathSegments = plan.op.path.split('/').filter((s) => s && !s.startsWith('{'));
|
|
297
|
-
const pathWords = new Set(pathSegments.flatMap((s) => s.split('_')).map(singularize));
|
|
298
|
-
let bestMatch: string | null = null;
|
|
299
|
-
let bestLen = 0;
|
|
300
|
-
let ambiguous = false;
|
|
301
|
-
for (const name of available) {
|
|
302
|
-
if (!verbMatches(name, plan.op.httpMethod, specVerb)) continue;
|
|
303
|
-
const methodWords = splitCamelWords(name).slice(1).map(singularize);
|
|
304
|
-
if (methodWords.length === 0) continue;
|
|
305
|
-
if (!methodWords.every((w) => pathWords.has(w))) continue;
|
|
306
|
-
if (methodWords.length < 2 && pathSegments.length > 2) continue;
|
|
307
|
-
if (methodWords.length > bestLen) {
|
|
308
|
-
bestMatch = name;
|
|
309
|
-
bestLen = methodWords.length;
|
|
310
|
-
ambiguous = false;
|
|
311
|
-
} else if (methodWords.length === bestLen) {
|
|
312
|
-
ambiguous = true;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
if (bestMatch && !ambiguous) {
|
|
316
|
-
resolved.set(plan, bestMatch);
|
|
317
|
-
available.delete(bestMatch);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Apply resolved names (only for non-overlay plans)
|
|
322
|
-
for (const plan of plans) {
|
|
323
|
-
if (overlayResolved.has(plan)) continue;
|
|
324
|
-
const name = resolved.get(plan);
|
|
325
|
-
if (name) plan.method = name;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
129
|
/**
|
|
330
130
|
* Deduplicate method names within the plans array.
|
|
331
131
|
*
|
|
@@ -393,11 +193,75 @@ function deduplicateMethodNames(
|
|
|
393
193
|
}
|
|
394
194
|
}
|
|
395
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
|
+
|
|
396
252
|
export function generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[] {
|
|
397
253
|
if (services.length === 0) return [];
|
|
398
254
|
const files: GeneratedFile[] = [];
|
|
399
255
|
|
|
400
|
-
|
|
256
|
+
// Group services by mount target to avoid file path collisions when
|
|
257
|
+
// multiple IR services mount to the same resource class.
|
|
258
|
+
const mountGroups = groupByMount(ctx);
|
|
259
|
+
const mergedServices: Service[] =
|
|
260
|
+
mountGroups.size > 0 ? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations })) : services;
|
|
261
|
+
|
|
262
|
+
const topLevelEnumNames = new Set(ctx.spec.enums.map((e) => e.name));
|
|
263
|
+
|
|
264
|
+
for (const service of mergedServices) {
|
|
401
265
|
if (isServiceCoveredByExisting(service, ctx)) {
|
|
402
266
|
// Fully covered: generate with ALL operations so the merger's docstring
|
|
403
267
|
// refresh pass can update JSDoc on existing methods.
|
|
@@ -406,6 +270,9 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
406
270
|
// remove skipIfExists so the merger adds the new methods.
|
|
407
271
|
if (hasMethodsAbsentFromBaseline(service, ctx)) {
|
|
408
272
|
delete file.skipIfExists;
|
|
273
|
+
// Suppress auto-generated header — the file is a merge target
|
|
274
|
+
// containing hand-written code, not a fully generated file.
|
|
275
|
+
file.headerPlacement = 'skip';
|
|
409
276
|
}
|
|
410
277
|
files.push(file);
|
|
411
278
|
continue;
|
|
@@ -420,12 +287,29 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
420
287
|
// merger adds new methods AND refreshes existing JSDoc.
|
|
421
288
|
const file = generateResourceClass(service, ctx);
|
|
422
289
|
delete file.skipIfExists;
|
|
290
|
+
// Suppress auto-generated header — the file is a merge target
|
|
291
|
+
// containing hand-written code, not a fully generated file.
|
|
292
|
+
file.headerPlacement = 'skip';
|
|
423
293
|
files.push(file);
|
|
424
294
|
} else {
|
|
425
|
-
|
|
295
|
+
// Purely oagen-managed: no baseline class exists, so the file is owned
|
|
296
|
+
// end-to-end by the emitter. Remove skipIfExists so regeneration always
|
|
297
|
+
// overwrites — emitter improvements (serializer dispatch, JSDoc, etc.)
|
|
298
|
+
// must propagate without manual intervention.
|
|
299
|
+
const file = generateResourceClass(service, ctx);
|
|
300
|
+
delete file.skipIfExists;
|
|
301
|
+
files.push(file);
|
|
426
302
|
}
|
|
427
303
|
}
|
|
428
304
|
|
|
305
|
+
// Emit paginated list options interfaces AFTER the resource classes so
|
|
306
|
+
// tests and manifest ordering that index `files[0]` as the class stay
|
|
307
|
+
// stable. Placing them under `interfaces/` lets the per-service barrel
|
|
308
|
+
// pick them up automatically.
|
|
309
|
+
for (const service of mergedServices) {
|
|
310
|
+
files.push(...generatePaginatedOptionsInterfaces(service, ctx, topLevelEnumNames));
|
|
311
|
+
}
|
|
312
|
+
|
|
429
313
|
return files;
|
|
430
314
|
}
|
|
431
315
|
|
|
@@ -441,10 +325,8 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
441
325
|
method: resolveMethodName(op, service, ctx),
|
|
442
326
|
}));
|
|
443
327
|
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
// spec-derived name doesn't match the hand-written SDK name.
|
|
447
|
-
reconcileMethodNames(plans, service, ctx);
|
|
328
|
+
// Resolved operations already produce correct method names via the
|
|
329
|
+
// centralized hint map — no per-emitter reconciliation needed.
|
|
448
330
|
|
|
449
331
|
// Deduplicate method names within the class (e.g., two operations both
|
|
450
332
|
// resolving to "create" for different paths).
|
|
@@ -458,18 +340,23 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
458
340
|
// order, JSDoc comments get attached to the wrong methods (list↔create,
|
|
459
341
|
// add↔set swaps). Sorting by the overlay's method order ensures the
|
|
460
342
|
// generated output matches the existing file's method order.
|
|
343
|
+
//
|
|
344
|
+
// We build the order from HTTP operation keys (e.g., "GET /organizations")
|
|
345
|
+
// rather than method names, because resolveMethodName may return a different
|
|
346
|
+
// name than the overlay's methodName (e.g., when the hint map overrides it),
|
|
347
|
+
// causing the lookup to fail and the sort to produce wrong order.
|
|
461
348
|
if (ctx.overlayLookup?.methodByOperation) {
|
|
462
|
-
const
|
|
349
|
+
const httpKeyOrder = new Map<string, number>();
|
|
463
350
|
let pos = 0;
|
|
464
|
-
for (const [
|
|
465
|
-
|
|
466
|
-
methodOrder.set(info.methodName, pos++);
|
|
467
|
-
}
|
|
351
|
+
for (const [httpKey] of ctx.overlayLookup.methodByOperation) {
|
|
352
|
+
httpKeyOrder.set(httpKey, pos++);
|
|
468
353
|
}
|
|
469
|
-
if (
|
|
354
|
+
if (httpKeyOrder.size > 0) {
|
|
470
355
|
plans.sort((a, b) => {
|
|
471
|
-
const
|
|
472
|
-
const
|
|
356
|
+
const aKey = `${a.op.httpMethod.toUpperCase()} ${a.op.path}`;
|
|
357
|
+
const bKey = `${b.op.httpMethod.toUpperCase()} ${b.op.path}`;
|
|
358
|
+
const aPos = httpKeyOrder.get(aKey) ?? Number.MAX_SAFE_INTEGER;
|
|
359
|
+
const bPos = httpKeyOrder.get(bKey) ?? Number.MAX_SAFE_INTEGER;
|
|
473
360
|
return aPos - bPos;
|
|
474
361
|
});
|
|
475
362
|
}
|
|
@@ -478,13 +365,32 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
478
365
|
const hasPaginated = plans.some((p) => p.plan.isPaginated);
|
|
479
366
|
const modelMap = new Map(ctx.spec.models.map((m) => [m.name, m]));
|
|
480
367
|
|
|
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
|
+
|
|
481
380
|
// Collect models for imports — only include models that are actually used
|
|
482
381
|
// in method signatures (not all union variants from the spec)
|
|
483
382
|
const responseModels = new Set<string>();
|
|
484
383
|
const requestModels = new Set<string>();
|
|
485
384
|
const paramEnums = new Set<string>();
|
|
486
385
|
const paramModels = new Set<string>();
|
|
487
|
-
for (const { op, plan } of plans) {
|
|
386
|
+
for (const { op, plan, method } of plans) {
|
|
387
|
+
// Skip imports for methods that already exist in the baseline class.
|
|
388
|
+
// The merger keeps baseline method bodies, so their imports are already
|
|
389
|
+
// present in the existing file. Including them here would create
|
|
390
|
+
// orphaned imports when the generated return type differs from the
|
|
391
|
+
// baseline's (e.g., generated `List` vs baseline `RoleList`).
|
|
392
|
+
if (baselineMethodSet.has(method)) continue;
|
|
393
|
+
|
|
488
394
|
if (plan.isPaginated && op.pagination?.itemType.kind === 'model') {
|
|
489
395
|
// For paginated operations, import the item type (e.g., Connection)
|
|
490
396
|
// rather than the list wrapper type (e.g., ConnectionList).
|
|
@@ -531,6 +437,28 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
531
437
|
collectParamTypeRefs(param.type, paramEnums, paramModels);
|
|
532
438
|
}
|
|
533
439
|
}
|
|
440
|
+
|
|
441
|
+
// Collect response models from union split wrappers so their types and
|
|
442
|
+
// deserializers are imported alongside the primary operation models.
|
|
443
|
+
// Also collect models referenced in wrapper param signatures (e.g.,
|
|
444
|
+
// `redirect_uris: RedirectUriInput[]`) — otherwise the wrapper emits a
|
|
445
|
+
// reference to a type it never imported.
|
|
446
|
+
const resolvedLookup = buildResolvedLookup(ctx);
|
|
447
|
+
for (const { op, method } of plans) {
|
|
448
|
+
if (baselineMethodSet.has(method)) continue;
|
|
449
|
+
const resolved = lookupResolved(op, resolvedLookup);
|
|
450
|
+
if (resolved) {
|
|
451
|
+
for (const name of collectWrapperResponseModels(resolved)) {
|
|
452
|
+
responseModels.add(name);
|
|
453
|
+
}
|
|
454
|
+
for (const wrapper of resolved.wrappers ?? []) {
|
|
455
|
+
for (const { field } of resolveWrapperParams(wrapper, ctx)) {
|
|
456
|
+
if (field) collectParamTypeRefs(field.type, paramEnums, paramModels);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
534
462
|
const allModels = new Set([...responseModels, ...requestModels, ...paramModels]);
|
|
535
463
|
|
|
536
464
|
const lines: string[] = [];
|
|
@@ -543,6 +471,17 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
543
471
|
lines.push("import { createPaginatedList } from '../common/utils/fetch-and-deserialize';");
|
|
544
472
|
}
|
|
545
473
|
|
|
474
|
+
// Paginated list options live in their own interface files so they're
|
|
475
|
+
// picked up by the per-service barrel (and flow through to the root
|
|
476
|
+
// package barrel). Import them here rather than declaring inline.
|
|
477
|
+
for (const { op, plan, method } of plans) {
|
|
478
|
+
if (!plan.isPaginated) continue;
|
|
479
|
+
const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
|
|
480
|
+
if (extraParams.length === 0) continue;
|
|
481
|
+
const optionsName = paginatedOptionsName(method, resolvedName);
|
|
482
|
+
lines.push(`import type { ${optionsName} } from './interfaces/${fileName(optionsName)}.interface';`);
|
|
483
|
+
}
|
|
484
|
+
|
|
546
485
|
// Check if any operation needs PostOptions (idempotent POST or custom encoding)
|
|
547
486
|
const hasIdempotentPost = plans.some((p) => p.plan.isIdempotentPost);
|
|
548
487
|
const hasCustomEncoding = plans.some(
|
|
@@ -646,21 +585,58 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
646
585
|
|
|
647
586
|
lines.push('');
|
|
648
587
|
|
|
649
|
-
//
|
|
650
|
-
//
|
|
588
|
+
// Per-operation helpers (wire-format option serializers etc.) emitted
|
|
589
|
+
// alongside the resource class. The options interfaces themselves live
|
|
590
|
+
// in separate files under `interfaces/` so the per-service barrel can
|
|
591
|
+
// re-export them; see the earlier import block at the top of the file.
|
|
651
592
|
for (const { op, plan, method } of plans) {
|
|
652
593
|
if (plan.isPaginated) {
|
|
653
594
|
const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
|
|
654
595
|
if (extraParams.length > 0) {
|
|
655
596
|
const optionsName = paginatedOptionsName(method, resolvedName);
|
|
656
|
-
|
|
657
|
-
//
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
//
|
|
662
|
-
|
|
663
|
-
|
|
597
|
+
|
|
598
|
+
// When any extension param has a camelCase domain name that differs
|
|
599
|
+
// from its snake_case wire name, emit a serializer that translates
|
|
600
|
+
// the user-facing options into the wire query shape. Without this,
|
|
601
|
+
// the query string uses camelCase keys (e.g. `organizationId=...`)
|
|
602
|
+
// that the API silently ignores — the filter becomes a no-op.
|
|
603
|
+
const needsWireSerializer = extraParams.some((p) => fieldName(p.name) !== wireFieldName(p.name));
|
|
604
|
+
if (needsWireSerializer) {
|
|
605
|
+
const serializerName = `serialize${optionsName}`;
|
|
606
|
+
lines.push(`const ${serializerName} = (options: ${optionsName}): PaginationOptions => {`);
|
|
607
|
+
// Pagination fields pass through unchanged (limit/before/after/order
|
|
608
|
+
// share spelling in both cases). Spread first so that wire-named
|
|
609
|
+
// extension fields land on top and the camelCase keys don't also
|
|
610
|
+
// leak into the query string.
|
|
611
|
+
lines.push(' const wire: Record<string, unknown> = {');
|
|
612
|
+
for (const p of PAGINATION_PARAM_NAMES) {
|
|
613
|
+
lines.push(` ${p}: options.${p},`);
|
|
614
|
+
}
|
|
615
|
+
lines.push(' };');
|
|
616
|
+
for (const param of extraParams) {
|
|
617
|
+
const camel = fieldName(param.name);
|
|
618
|
+
const snake = wireFieldName(param.name);
|
|
619
|
+
lines.push(` if (options.${camel} !== undefined) wire.${snake} = options.${camel};`);
|
|
620
|
+
}
|
|
621
|
+
lines.push(' return wire as PaginationOptions;');
|
|
622
|
+
lines.push('};');
|
|
623
|
+
lines.push('');
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
} else if (!plan.isPaginated && !plan.hasBody && !plan.isDelete && op.queryParams.length > 0) {
|
|
627
|
+
// Non-paginated GET or void methods with query params get a typed options interface
|
|
628
|
+
// instead of falling back to Record<string, unknown>.
|
|
629
|
+
// Filter out hidden params (defaults and inferFromClient fields)
|
|
630
|
+
const resolved = lookupResolved(op, resolvedLookup);
|
|
631
|
+
const opHiddenParams = new Set<string>([
|
|
632
|
+
...Object.keys(getOpDefaults(resolved)),
|
|
633
|
+
...getOpInferFromClient(resolved),
|
|
634
|
+
]);
|
|
635
|
+
const visibleParams = op.queryParams.filter((p) => !opHiddenParams.has(p.name));
|
|
636
|
+
if (visibleParams.length > 0) {
|
|
637
|
+
const optionsName = toPascalCase(method) + 'Options';
|
|
638
|
+
lines.push(`export interface ${optionsName} {`);
|
|
639
|
+
for (const param of visibleParams) {
|
|
664
640
|
const opt = !param.required ? '?' : '';
|
|
665
641
|
if (param.description || param.deprecated) {
|
|
666
642
|
const parts: string[] = [];
|
|
@@ -673,23 +649,6 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
673
649
|
lines.push('}');
|
|
674
650
|
lines.push('');
|
|
675
651
|
}
|
|
676
|
-
} else if (!plan.isPaginated && !plan.hasBody && !plan.isDelete && op.queryParams.length > 0) {
|
|
677
|
-
// Non-paginated GET or void methods with query params get a typed options interface
|
|
678
|
-
// instead of falling back to Record<string, unknown>.
|
|
679
|
-
const optionsName = toPascalCase(method) + 'Options';
|
|
680
|
-
lines.push(`export interface ${optionsName} {`);
|
|
681
|
-
for (const param of op.queryParams) {
|
|
682
|
-
const opt = !param.required ? '?' : '';
|
|
683
|
-
if (param.description || param.deprecated) {
|
|
684
|
-
const parts: string[] = [];
|
|
685
|
-
if (param.description) parts.push(param.description);
|
|
686
|
-
if (param.deprecated) parts.push('@deprecated');
|
|
687
|
-
lines.push(...docComment(parts.join('\n'), 2));
|
|
688
|
-
}
|
|
689
|
-
lines.push(` ${fieldName(param.name)}${opt}: ${mapParamType(param.type, specEnumNames)};`);
|
|
690
|
-
}
|
|
691
|
-
lines.push('}');
|
|
692
|
-
lines.push('');
|
|
693
652
|
}
|
|
694
653
|
}
|
|
695
654
|
|
|
@@ -702,7 +661,13 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
702
661
|
|
|
703
662
|
for (const { op, plan, method } of plans) {
|
|
704
663
|
lines.push('');
|
|
705
|
-
|
|
664
|
+
const resolved = lookupResolved(op, resolvedLookup);
|
|
665
|
+
lines.push(...renderMethod(op, plan, method, service, ctx, modelMap, specEnumNames, resolved));
|
|
666
|
+
|
|
667
|
+
// Emit union split wrapper methods (typed convenience methods for each variant)
|
|
668
|
+
if (resolved?.wrappers && resolved.wrappers.length > 0) {
|
|
669
|
+
lines.push(...generateWrapperMethods(resolved, ctx));
|
|
670
|
+
}
|
|
706
671
|
}
|
|
707
672
|
|
|
708
673
|
lines.push('}');
|
|
@@ -718,12 +683,20 @@ function renderMethod(
|
|
|
718
683
|
ctx: EmitterContext,
|
|
719
684
|
modelMap: Map<string, Model>,
|
|
720
685
|
specEnumNames?: Set<string>,
|
|
686
|
+
resolvedOp?: ResolvedOperation,
|
|
721
687
|
): string[] {
|
|
722
688
|
const lines: string[] = [];
|
|
723
689
|
const responseModel = plan.responseModelName ? resolveInterfaceName(plan.responseModelName, ctx) : null;
|
|
724
690
|
|
|
725
691
|
const pathStr = buildPathStr(op);
|
|
726
692
|
|
|
693
|
+
// Build the set of params hidden from the method signature
|
|
694
|
+
// (injected from client config or as constant defaults)
|
|
695
|
+
const hiddenParams = new Set<string>([
|
|
696
|
+
...Object.keys(getOpDefaults(resolvedOp)),
|
|
697
|
+
...getOpInferFromClient(resolvedOp),
|
|
698
|
+
]);
|
|
699
|
+
|
|
727
700
|
// Build set of valid param names to filter @param tags.
|
|
728
701
|
// Prefer the overlay (existing method signature) if available;
|
|
729
702
|
// otherwise compute from what the render path will actually include.
|
|
@@ -739,119 +712,136 @@ function renderMethod(
|
|
|
739
712
|
for (const p of op.pathParams) actualParams.add(fieldName(p.name));
|
|
740
713
|
if (plan.hasBody) actualParams.add('payload');
|
|
741
714
|
if (plan.isPaginated) actualParams.add('options');
|
|
742
|
-
// renderGetMethod
|
|
743
|
-
|
|
715
|
+
// renderGetMethod/renderVoidMethod add options when there are visible non-paginated query params
|
|
716
|
+
const visibleQueryCount = op.queryParams.filter((q) => !hiddenParams.has(q.name)).length;
|
|
717
|
+
if (!plan.isPaginated && visibleQueryCount > 0 && !plan.isDelete) {
|
|
744
718
|
actualParams.add('options');
|
|
745
719
|
}
|
|
746
720
|
validParamNames = actualParams;
|
|
747
721
|
}
|
|
748
722
|
|
|
749
|
-
|
|
750
|
-
if
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
723
|
+
// Always generate JSDoc for all methods (both existing and new).
|
|
724
|
+
// The merger matches docstrings by member name — if we skip JSDoc for
|
|
725
|
+
// existing methods, previously misplaced docstrings can never be corrected.
|
|
726
|
+
// Hand-written docs (e.g., @deprecated, PKCE flow descriptions) are
|
|
727
|
+
// preserved by the merger's @deprecated-preservation and @oagen-ignore
|
|
728
|
+
// mechanisms instead.
|
|
729
|
+
{
|
|
730
|
+
const docParts: string[] = [];
|
|
731
|
+
if (op.description) docParts.push(op.description);
|
|
732
|
+
for (const param of op.pathParams) {
|
|
733
|
+
const paramName = fieldName(param.name);
|
|
734
|
+
if (validParamNames && !validParamNames.has(paramName)) continue;
|
|
735
|
+
const deprecatedPrefix = param.deprecated ? '(deprecated) ' : '';
|
|
736
|
+
if (param.description) {
|
|
737
|
+
docParts.push(`@param ${paramName} - ${deprecatedPrefix}${param.description}`);
|
|
738
|
+
} else if (param.deprecated) {
|
|
739
|
+
docParts.push(`@param ${paramName} - (deprecated)`);
|
|
740
|
+
}
|
|
741
|
+
if (param.default !== undefined) docParts.push(`@default ${JSON.stringify(param.default)}`);
|
|
742
|
+
if (param.example !== undefined) docParts.push(`@example ${JSON.stringify(param.example)}`);
|
|
759
743
|
}
|
|
760
|
-
|
|
761
|
-
if (
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
744
|
+
// Document query params for non-paginated operations
|
|
745
|
+
if (!plan.isPaginated) {
|
|
746
|
+
// Only document query params if the method will have an options parameter
|
|
747
|
+
if (validParamNames && (validParamNames.has('options') || overlayMethod)) {
|
|
748
|
+
for (const param of op.queryParams) {
|
|
749
|
+
if (hiddenParams.has(param.name)) continue;
|
|
750
|
+
const paramName = `options.${fieldName(param.name)}`;
|
|
751
|
+
if (validParamNames && !validParamNames.has('options') && !validParamNames.has(fieldName(param.name)))
|
|
752
|
+
continue;
|
|
753
|
+
const deprecatedPrefix = param.deprecated ? '(deprecated) ' : '';
|
|
754
|
+
if (param.description) {
|
|
755
|
+
docParts.push(`@param ${paramName} - ${deprecatedPrefix}${param.description}`);
|
|
756
|
+
} else if (param.deprecated) {
|
|
757
|
+
docParts.push(`@param ${paramName} - (deprecated)`);
|
|
758
|
+
}
|
|
759
|
+
if (param.default !== undefined) docParts.push(`@default ${JSON.stringify(param.default)}`);
|
|
760
|
+
if (param.example !== undefined) docParts.push(`@example ${JSON.stringify(param.example)}`);
|
|
775
761
|
}
|
|
776
|
-
if (param.default !== undefined) docParts.push(`@default ${JSON.stringify(param.default)}`);
|
|
777
|
-
if (param.example !== undefined) docParts.push(`@example ${JSON.stringify(param.example)}`);
|
|
778
762
|
}
|
|
779
763
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
764
|
+
// Skip header and cookie params in JSDoc — they are not exposed in the method signature.
|
|
765
|
+
// The SDK handles headers and cookies internally, so documenting them would be misleading.
|
|
766
|
+
// Document payload parameter when there is a request body
|
|
767
|
+
if (plan.hasBody) {
|
|
768
|
+
const bodyInfo = extractRequestBodyType(op, ctx);
|
|
769
|
+
if (bodyInfo?.kind === 'model') {
|
|
770
|
+
const bodyModel = ctx.spec.models.find((m) => m.name === bodyInfo.name);
|
|
771
|
+
let payloadDesc: string;
|
|
772
|
+
if (bodyModel?.description) {
|
|
773
|
+
payloadDesc = `@param payload - ${bodyModel.description}`;
|
|
774
|
+
} else if (bodyModel) {
|
|
775
|
+
// When the model lacks a description, list its required fields to help
|
|
776
|
+
// callers understand what must be provided.
|
|
777
|
+
const requiredFieldNames = bodyModel.fields.filter((f) => f.required).map((f) => fieldName(f.name));
|
|
778
|
+
payloadDesc =
|
|
779
|
+
requiredFieldNames.length > 0
|
|
780
|
+
? `@param payload - Object containing ${requiredFieldNames.join(', ')}.`
|
|
781
|
+
: '@param payload - The request body.';
|
|
782
|
+
} else {
|
|
783
|
+
payloadDesc = '@param payload - The request body.';
|
|
784
|
+
}
|
|
785
|
+
docParts.push(payloadDesc);
|
|
799
786
|
} else {
|
|
800
|
-
|
|
787
|
+
docParts.push('@param payload - The request body.');
|
|
801
788
|
}
|
|
802
|
-
docParts.push(payloadDesc);
|
|
803
|
-
} else {
|
|
804
|
-
docParts.push('@param payload - The request body.');
|
|
805
789
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
docParts.push('@param options - Additional query options.');
|
|
812
|
-
}
|
|
813
|
-
// @returns for the primary response model (use item type for paginated operations).
|
|
814
|
-
// Unwrap list wrapper models to match the actual return type — the method returns
|
|
815
|
-
// AutoPaginatable<ItemType>, not the list wrapper.
|
|
816
|
-
if (plan.isPaginated && op.pagination?.itemType.kind === 'model') {
|
|
817
|
-
let itemRawName = op.pagination.itemType.name;
|
|
818
|
-
const pModel = modelMap.get(itemRawName);
|
|
819
|
-
if (pModel) {
|
|
820
|
-
const unwrapped = unwrapListModel(pModel, modelMap);
|
|
821
|
-
if (unwrapped) itemRawName = unwrapped.name;
|
|
790
|
+
// Document options parameter for paginated operations
|
|
791
|
+
if (plan.isPaginated) {
|
|
792
|
+
docParts.push('@param options - Pagination and filter options.');
|
|
793
|
+
} else if (op.queryParams.filter((q) => !hiddenParams.has(q.name)).length > 0) {
|
|
794
|
+
docParts.push('@param options - Additional query options.');
|
|
822
795
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
796
|
+
// @returns for the primary response model.
|
|
797
|
+
// When an overlay method exists, prefer its return type so the JSDoc
|
|
798
|
+
// matches the actual TypeScript signature (the overlay may use a
|
|
799
|
+
// different model name than the OpenAPI schema).
|
|
800
|
+
if (overlayMethod?.returnType) {
|
|
801
|
+
docParts.push(`@returns {${overlayMethod.returnType}}`);
|
|
802
|
+
} else if (plan.isPaginated && op.pagination?.itemType.kind === 'model') {
|
|
803
|
+
// Unwrap list wrapper models to match the actual return type — the method returns
|
|
804
|
+
// AutoPaginatable<ItemType>, not the list wrapper.
|
|
805
|
+
let itemRawName = op.pagination.itemType.name;
|
|
806
|
+
const pModel = modelMap.get(itemRawName);
|
|
807
|
+
if (pModel) {
|
|
808
|
+
const unwrapped = unwrapListModel(pModel, modelMap);
|
|
809
|
+
if (unwrapped) itemRawName = unwrapped.name;
|
|
810
|
+
}
|
|
811
|
+
const itemTypeName = resolveInterfaceName(itemRawName, ctx);
|
|
812
|
+
docParts.push(`@returns {Promise<AutoPaginatable<${itemTypeName}>>}`);
|
|
813
|
+
} else if (responseModel) {
|
|
814
|
+
const returnTypeDoc = plan.isArrayResponse ? `${responseModel}[]` : responseModel;
|
|
815
|
+
docParts.push(`@returns {Promise<${returnTypeDoc}>}`);
|
|
816
|
+
} else {
|
|
817
|
+
docParts.push('@returns {Promise<void>}');
|
|
835
818
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
const allLines: string[] = [];
|
|
842
|
-
for (const part of docParts) {
|
|
843
|
-
for (const line of part.split('\n')) {
|
|
844
|
-
allLines.push(line);
|
|
819
|
+
// @throws for error responses
|
|
820
|
+
for (const err of op.errors) {
|
|
821
|
+
const exceptionName = STATUS_TO_EXCEPTION_NAME[err.statusCode];
|
|
822
|
+
if (exceptionName) {
|
|
823
|
+
docParts.push(`@throws {${exceptionName}} ${err.statusCode}`);
|
|
845
824
|
}
|
|
846
825
|
}
|
|
847
|
-
if (
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
lines
|
|
851
|
-
|
|
852
|
-
|
|
826
|
+
if (op.deprecated) docParts.push('@deprecated');
|
|
827
|
+
|
|
828
|
+
if (docParts.length > 0) {
|
|
829
|
+
// Flatten all parts, splitting multiline descriptions into individual lines
|
|
830
|
+
const allLines: string[] = [];
|
|
831
|
+
for (const part of docParts) {
|
|
832
|
+
for (const line of part.split('\n')) {
|
|
833
|
+
allLines.push(line);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (allLines.length === 1) {
|
|
837
|
+
lines.push(` /** ${allLines[0]} */`);
|
|
838
|
+
} else {
|
|
839
|
+
lines.push(' /**');
|
|
840
|
+
for (const line of allLines) {
|
|
841
|
+
lines.push(line === '' ? ' *' : ` * ${line}`);
|
|
842
|
+
}
|
|
843
|
+
lines.push(' */');
|
|
853
844
|
}
|
|
854
|
-
lines.push(' */');
|
|
855
845
|
}
|
|
856
846
|
}
|
|
857
847
|
|
|
@@ -895,9 +885,9 @@ function renderMethod(
|
|
|
895
885
|
} else if (plan.hasBody && responseModel) {
|
|
896
886
|
renderBodyMethod(lines, op, plan, method, responseModel, pathStr, ctx, specEnumNames);
|
|
897
887
|
} else if (responseModel) {
|
|
898
|
-
renderGetMethod(lines, op, plan, method, responseModel, pathStr, specEnumNames);
|
|
888
|
+
renderGetMethod(lines, op, plan, method, responseModel, pathStr, specEnumNames, resolvedOp);
|
|
899
889
|
} else {
|
|
900
|
-
renderVoidMethod(lines, op, plan, method, pathStr, ctx, specEnumNames);
|
|
890
|
+
renderVoidMethod(lines, op, plan, method, pathStr, ctx, specEnumNames, resolvedOp);
|
|
901
891
|
}
|
|
902
892
|
|
|
903
893
|
// Defensive: if no render function produced a method body, emit a stub
|
|
@@ -925,13 +915,18 @@ function renderPaginatedMethod(
|
|
|
925
915
|
): void {
|
|
926
916
|
const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
|
|
927
917
|
const optionsType = extraParams.length > 0 ? paginatedOptionsName(method, resolvedServiceName) : 'PaginationOptions';
|
|
918
|
+
// When any extension param has a camelCase/snake_case divergence, the
|
|
919
|
+
// resource file emits a `serialize<OptionsName>` helper — pass it to
|
|
920
|
+
// createPaginatedList so the wire query uses snake_case keys.
|
|
921
|
+
const needsWireSerializer = extraParams.some((p) => fieldName(p.name) !== wireFieldName(p.name));
|
|
922
|
+
const serializerArg = needsWireSerializer ? `, serialize${optionsType}` : '';
|
|
928
923
|
|
|
929
924
|
const pathParams = buildPathParams(op, specEnumNames);
|
|
930
925
|
const allParams = pathParams ? `${pathParams}, options?: ${optionsType}` : `options?: ${optionsType}`;
|
|
931
926
|
|
|
932
927
|
lines.push(` async ${method}(${allParams}): Promise<AutoPaginatable<${itemType}, ${optionsType}>> {`);
|
|
933
928
|
lines.push(
|
|
934
|
-
` return createPaginatedList<${wireInterfaceName(itemType)}, ${itemType}, ${optionsType}>(this.workos, ${pathStr}, deserialize${itemType}, options);`,
|
|
929
|
+
` return createPaginatedList<${wireInterfaceName(itemType)}, ${itemType}, ${optionsType}>(this.workos, ${pathStr}, deserialize${itemType}, options${serializerArg});`,
|
|
935
930
|
);
|
|
936
931
|
lines.push(' }');
|
|
937
932
|
}
|
|
@@ -1040,16 +1035,22 @@ function renderBodyMethod(
|
|
|
1040
1035
|
const encodingOption = encoding && encoding !== 'json' ? `, encoding: '${encoding}' as const` : '';
|
|
1041
1036
|
const hasCustomEncoding = encodingOption !== '';
|
|
1042
1037
|
|
|
1043
|
-
|
|
1038
|
+
const returnType = plan.isArrayResponse ? `${responseModel}[]` : responseModel;
|
|
1039
|
+
const wireType = plan.isArrayResponse ? `${wireInterfaceName(responseModel)}[]` : wireInterfaceName(responseModel);
|
|
1040
|
+
const returnExpr = plan.isArrayResponse
|
|
1041
|
+
? `data.map(deserialize${responseModel})`
|
|
1042
|
+
: `deserialize${responseModel}(data)`;
|
|
1043
|
+
|
|
1044
|
+
lines.push(` async ${method}(${paramsStr}): Promise<${returnType}> {`);
|
|
1044
1045
|
if (plan.isIdempotentPost) {
|
|
1045
1046
|
if (hasCustomEncoding) {
|
|
1046
|
-
lines.push(` const { data } = await this.workos.${op.httpMethod}<${
|
|
1047
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(`);
|
|
1047
1048
|
lines.push(` ${pathStr},`);
|
|
1048
1049
|
lines.push(` ${bodyExpr},`);
|
|
1049
1050
|
lines.push(` { ...requestOptions${encodingOption} },`);
|
|
1050
1051
|
lines.push(' );');
|
|
1051
1052
|
} else {
|
|
1052
|
-
lines.push(` const { data } = await this.workos.${op.httpMethod}<${
|
|
1053
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(`);
|
|
1053
1054
|
lines.push(` ${pathStr},`);
|
|
1054
1055
|
lines.push(` ${bodyExpr},`);
|
|
1055
1056
|
lines.push(' requestOptions,');
|
|
@@ -1057,19 +1058,19 @@ function renderBodyMethod(
|
|
|
1057
1058
|
}
|
|
1058
1059
|
} else {
|
|
1059
1060
|
if (hasCustomEncoding) {
|
|
1060
|
-
lines.push(` const { data } = await this.workos.${op.httpMethod}<${
|
|
1061
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(`);
|
|
1061
1062
|
lines.push(` ${pathStr},`);
|
|
1062
1063
|
lines.push(` ${bodyExpr},`);
|
|
1063
1064
|
lines.push(` { ${encodingOption.slice(2)} },`);
|
|
1064
1065
|
lines.push(' );');
|
|
1065
1066
|
} else {
|
|
1066
|
-
lines.push(` const { data } = await this.workos.${op.httpMethod}<${
|
|
1067
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(`);
|
|
1067
1068
|
lines.push(` ${pathStr},`);
|
|
1068
1069
|
lines.push(` ${bodyExpr},`);
|
|
1069
1070
|
lines.push(' );');
|
|
1070
1071
|
}
|
|
1071
1072
|
}
|
|
1072
|
-
lines.push(` return
|
|
1073
|
+
lines.push(` return ${returnExpr};`);
|
|
1073
1074
|
lines.push(' }');
|
|
1074
1075
|
}
|
|
1075
1076
|
|
|
@@ -1081,35 +1082,109 @@ function renderGetMethod(
|
|
|
1081
1082
|
responseModel: string,
|
|
1082
1083
|
pathStr: string,
|
|
1083
1084
|
specEnumNames?: Set<string>,
|
|
1085
|
+
resolvedOp?: ResolvedOperation,
|
|
1084
1086
|
): void {
|
|
1087
|
+
const hiddenParams = new Set<string>([
|
|
1088
|
+
...Object.keys(getOpDefaults(resolvedOp)),
|
|
1089
|
+
...getOpInferFromClient(resolvedOp),
|
|
1090
|
+
]);
|
|
1091
|
+
|
|
1085
1092
|
const params = buildPathParams(op, specEnumNames);
|
|
1086
|
-
const
|
|
1087
|
-
const
|
|
1093
|
+
const visibleQueryParams = op.queryParams.filter((p) => !hiddenParams.has(p.name));
|
|
1094
|
+
const hasVisibleQuery = visibleQueryParams.length > 0 && !plan.isPaginated;
|
|
1095
|
+
const hasDefaults = Object.keys(getOpDefaults(resolvedOp)).length > 0;
|
|
1096
|
+
const hasInferred = getOpInferFromClient(resolvedOp).length > 0;
|
|
1097
|
+
const hasInjected = hasDefaults || hasInferred;
|
|
1098
|
+
const hasQuery = (op.queryParams.length > 0 && !plan.isPaginated) || hasInjected;
|
|
1099
|
+
const optionsType = hasVisibleQuery ? toPascalCase(method) + 'Options' : null;
|
|
1100
|
+
|
|
1101
|
+
const allParams = optionsType
|
|
1102
|
+
? params
|
|
1103
|
+
? `${params}, options?: ${optionsType}`
|
|
1104
|
+
: `options?: ${optionsType}`
|
|
1105
|
+
: params;
|
|
1106
|
+
|
|
1107
|
+
const returnType = plan.isArrayResponse ? `${responseModel}[]` : responseModel;
|
|
1108
|
+
const wireType = plan.isArrayResponse ? `${wireInterfaceName(responseModel)}[]` : wireInterfaceName(responseModel);
|
|
1109
|
+
const returnExpr = plan.isArrayResponse
|
|
1110
|
+
? `data.map(deserialize${responseModel})`
|
|
1111
|
+
: `deserialize${responseModel}(data)`;
|
|
1112
|
+
|
|
1113
|
+
lines.push(` async ${method}(${allParams}): Promise<${returnType}> {`);
|
|
1114
|
+
if (hasQuery) {
|
|
1115
|
+
if (hasInjected) {
|
|
1116
|
+
// Build the query object with visible params, defaults, and inferred fields
|
|
1117
|
+
const queryParts: string[] = [];
|
|
1118
|
+
|
|
1119
|
+
// Regular visible query params (from options)
|
|
1120
|
+
if (hasVisibleQuery) {
|
|
1121
|
+
for (const param of visibleQueryParams) {
|
|
1122
|
+
const camel = fieldName(param.name);
|
|
1123
|
+
const snake = wireFieldName(param.name);
|
|
1124
|
+
if (camel === snake) {
|
|
1125
|
+
if (param.required) {
|
|
1126
|
+
queryParts.push(`${camel}: options.${camel}`);
|
|
1127
|
+
} else {
|
|
1128
|
+
queryParts.push(`...(options?.${camel} !== undefined && { ${camel}: options.${camel} })`);
|
|
1129
|
+
}
|
|
1130
|
+
} else {
|
|
1131
|
+
if (param.required) {
|
|
1132
|
+
queryParts.push(`${snake}: options.${camel}`);
|
|
1133
|
+
} else {
|
|
1134
|
+
queryParts.push(`...(options?.${camel} !== undefined && { ${snake}: options.${camel} })`);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1088
1139
|
|
|
1089
|
-
|
|
1140
|
+
// Constant defaults (e.g., response_type: 'code')
|
|
1141
|
+
for (const [key, value] of Object.entries(getOpDefaults(resolvedOp))) {
|
|
1142
|
+
queryParts.push(`${key}: ${tsLiteral(value)}`);
|
|
1143
|
+
}
|
|
1090
1144
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
);
|
|
1097
|
-
|
|
1098
|
-
|
|
1145
|
+
// Inferred fields from client config (e.g., client_id from this.workos.options.clientId)
|
|
1146
|
+
for (const field of getOpInferFromClient(resolvedOp)) {
|
|
1147
|
+
queryParts.push(`${field}: ${clientFieldExpression(field)}`);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr}, {`);
|
|
1151
|
+
lines.push(` query: { ${queryParts.join(', ')} },`);
|
|
1152
|
+
lines.push(' });');
|
|
1153
|
+
} else {
|
|
1154
|
+
const queryExpr = renderQueryExpr(visibleQueryParams);
|
|
1155
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr}, {`);
|
|
1156
|
+
lines.push(` query: ${queryExpr},`);
|
|
1157
|
+
lines.push(' });');
|
|
1158
|
+
}
|
|
1099
1159
|
} else if (httpMethodNeedsBody(op.httpMethod)) {
|
|
1100
1160
|
// PUT/PATCH/POST require a body argument even when the spec has no request body
|
|
1101
|
-
lines.push(
|
|
1102
|
-
` const { data } = await this.workos.${op.httpMethod}<${wireInterfaceName(responseModel)}>(${pathStr}, {});`,
|
|
1103
|
-
);
|
|
1161
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr}, {});`);
|
|
1104
1162
|
} else {
|
|
1105
|
-
lines.push(
|
|
1106
|
-
` const { data } = await this.workos.${op.httpMethod}<${wireInterfaceName(responseModel)}>(${pathStr});`,
|
|
1107
|
-
);
|
|
1163
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr});`);
|
|
1108
1164
|
}
|
|
1109
|
-
lines.push(` return
|
|
1165
|
+
lines.push(` return ${returnExpr};`);
|
|
1110
1166
|
lines.push(' }');
|
|
1111
1167
|
}
|
|
1112
1168
|
|
|
1169
|
+
/** Convert a JS value to a TypeScript literal. */
|
|
1170
|
+
function tsLiteral(value: string | number | boolean): string {
|
|
1171
|
+
if (typeof value === 'string') return `'${value.replace(/'/g, "\\'")}'`;
|
|
1172
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
1173
|
+
return String(value);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/** Get the TypeScript expression for reading a client config field. */
|
|
1177
|
+
function clientFieldExpression(field: string): string {
|
|
1178
|
+
switch (field) {
|
|
1179
|
+
case 'client_id':
|
|
1180
|
+
return 'this.workos.options.clientId';
|
|
1181
|
+
case 'client_secret':
|
|
1182
|
+
return 'this.workos.key';
|
|
1183
|
+
default:
|
|
1184
|
+
return `this.workos.${toCamelCase(field)}`;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1113
1188
|
function renderVoidMethod(
|
|
1114
1189
|
lines: string[],
|
|
1115
1190
|
op: Operation,
|
|
@@ -1118,10 +1193,21 @@ function renderVoidMethod(
|
|
|
1118
1193
|
pathStr: string,
|
|
1119
1194
|
ctx: EmitterContext,
|
|
1120
1195
|
specEnumNames?: Set<string>,
|
|
1196
|
+
resolvedOp?: ResolvedOperation,
|
|
1121
1197
|
): void {
|
|
1198
|
+
const hiddenParams = new Set<string>([
|
|
1199
|
+
...Object.keys(getOpDefaults(resolvedOp)),
|
|
1200
|
+
...getOpInferFromClient(resolvedOp),
|
|
1201
|
+
]);
|
|
1202
|
+
|
|
1122
1203
|
const params = buildPathParams(op, specEnumNames);
|
|
1123
|
-
const
|
|
1124
|
-
const
|
|
1204
|
+
const visibleQueryParams = op.queryParams.filter((p) => !hiddenParams.has(p.name));
|
|
1205
|
+
const hasVisibleQuery = visibleQueryParams.length > 0 && !plan.hasBody;
|
|
1206
|
+
const hasDefaults = Object.keys(getOpDefaults(resolvedOp)).length > 0;
|
|
1207
|
+
const hasInferred = getOpInferFromClient(resolvedOp).length > 0;
|
|
1208
|
+
const hasInjected = hasDefaults || hasInferred;
|
|
1209
|
+
const hasQuery = hasVisibleQuery || (hasInjected && !plan.hasBody);
|
|
1210
|
+
const optionsType = hasVisibleQuery ? toPascalCase(method) + 'Options' : null;
|
|
1125
1211
|
|
|
1126
1212
|
let bodyParam = '';
|
|
1127
1213
|
let bodyExpr = 'payload';
|
|
@@ -1154,10 +1240,47 @@ function renderVoidMethod(
|
|
|
1154
1240
|
if (plan.hasBody) {
|
|
1155
1241
|
lines.push(` await this.workos.${op.httpMethod}(${pathStr}, ${bodyExpr});`);
|
|
1156
1242
|
} else if (hasQuery) {
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1243
|
+
if (hasInjected) {
|
|
1244
|
+
// Build query object with visible params, defaults, and inferred fields
|
|
1245
|
+
const queryParts: string[] = [];
|
|
1246
|
+
|
|
1247
|
+
if (hasVisibleQuery) {
|
|
1248
|
+
for (const param of visibleQueryParams) {
|
|
1249
|
+
const camel = fieldName(param.name);
|
|
1250
|
+
const snake = wireFieldName(param.name);
|
|
1251
|
+
if (camel === snake) {
|
|
1252
|
+
if (param.required) {
|
|
1253
|
+
queryParts.push(`${camel}: options.${camel}`);
|
|
1254
|
+
} else {
|
|
1255
|
+
queryParts.push(`...(options?.${camel} !== undefined && { ${camel}: options.${camel} })`);
|
|
1256
|
+
}
|
|
1257
|
+
} else {
|
|
1258
|
+
if (param.required) {
|
|
1259
|
+
queryParts.push(`${snake}: options.${camel}`);
|
|
1260
|
+
} else {
|
|
1261
|
+
queryParts.push(`...(options?.${camel} !== undefined && { ${snake}: options.${camel} })`);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
for (const [key, value] of Object.entries(getOpDefaults(resolvedOp))) {
|
|
1268
|
+
queryParts.push(`${key}: ${tsLiteral(value)}`);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
for (const field of getOpInferFromClient(resolvedOp)) {
|
|
1272
|
+
queryParts.push(`${field}: ${clientFieldExpression(field)}`);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
lines.push(` await this.workos.${op.httpMethod}(${pathStr}, {`);
|
|
1276
|
+
lines.push(` query: { ${queryParts.join(', ')} },`);
|
|
1277
|
+
lines.push(' });');
|
|
1278
|
+
} else {
|
|
1279
|
+
const queryExpr = renderQueryExpr(visibleQueryParams);
|
|
1280
|
+
lines.push(` await this.workos.${op.httpMethod}(${pathStr}, {`);
|
|
1281
|
+
lines.push(` query: ${queryExpr},`);
|
|
1282
|
+
lines.push(' });');
|
|
1283
|
+
}
|
|
1161
1284
|
} else if (httpMethodNeedsBody(op.httpMethod)) {
|
|
1162
1285
|
lines.push(` await this.workos.${op.httpMethod}(${pathStr}, {});`);
|
|
1163
1286
|
} else {
|
|
@@ -1253,9 +1376,16 @@ function renderUnionBodySerializer(
|
|
|
1253
1376
|
const cases: string[] = [];
|
|
1254
1377
|
for (const [value, modelName] of Object.entries(disc.mapping)) {
|
|
1255
1378
|
const resolved = resolveInterfaceName(modelName, ctx);
|
|
1256
|
-
|
|
1379
|
+
// Switch on a typed discriminator narrows `payload` to the variant, so the
|
|
1380
|
+
// serializer call type-checks without any casts.
|
|
1381
|
+
cases.push(`case '${value}': return serialize${resolved}(payload)`);
|
|
1257
1382
|
}
|
|
1258
|
-
|
|
1383
|
+
// Assign `payload` to `never` in the default branch to get a compile-time
|
|
1384
|
+
// exhaustiveness check — if a new variant is added to the union but not to
|
|
1385
|
+
// the switch, the build fails here. At runtime, we still throw so an
|
|
1386
|
+
// unknown discriminator slipping through via `as any` fails loudly rather
|
|
1387
|
+
// than silently forwarding camelCase to the API.
|
|
1388
|
+
return `(() => { switch (payload.${prop}) { ${cases.join('; ')}; default: { const _unknown: never = payload; throw new Error(\`Unknown ${prop}: \${(_unknown as { ${prop}?: unknown }).${prop}}\`) } } })()`;
|
|
1259
1389
|
}
|
|
1260
1390
|
|
|
1261
1391
|
/**
|