@workos/oagen-emitters 0.2.1 → 0.3.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 +8 -0
- package/README.md +129 -0
- package/dist/index.d.mts +10 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +11893 -3226
- package/dist/index.mjs.map +1 -1
- 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 +298 -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-go.ts +116 -42
- package/smoke/sdk-php.ts +28 -26
- package/smoke/sdk-python.ts +5 -2
- 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 +81 -0
- package/src/go/manifest.ts +36 -0
- package/src/go/models.ts +254 -0
- package/src/go/naming.ts +191 -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 +3 -0
- package/src/node/client.ts +78 -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 +2 -9
- package/src/node/models.ts +178 -21
- package/src/node/naming.ts +49 -111
- package/src/node/resources.ts +374 -364
- package/src/node/sdk-errors.ts +41 -0
- package/src/node/tests.ts +32 -12
- 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 +171 -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 +298 -0
- package/src/php/resources.ts +561 -0
- package/src/php/tests.ts +533 -0
- package/src/php/type-map.ts +90 -0
- package/src/php/utils.ts +18 -0
- package/src/php/wrappers.ts +151 -0
- package/src/python/client.ts +337 -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 +209 -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 +255 -0
- package/src/shared/naming-utils.ts +107 -0
- package/src/shared/non-spec-services.ts +54 -0
- package/src/shared/resolved-ops.ts +109 -0
- package/src/shared/wrapper-utils.ts +59 -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/node/client.test.ts +18 -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 +99 -69
- package/test/node/serializers.test.ts +3 -1
- package/test/node/type-map.test.ts +11 -0
- package/test/php/client.test.ts +94 -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 +644 -0
- package/test/php/tests.test.ts +118 -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,15 @@ 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';
|
|
26
43
|
|
|
27
44
|
/**
|
|
28
45
|
* Check whether the baseline (hand-written) class has a constructor compatible
|
|
@@ -63,15 +80,9 @@ export function resolveResourceClassName(service: Service, ctx: EmitterContext):
|
|
|
63
80
|
/** Standard pagination query params handled by PaginationOptions — not imported individually. */
|
|
64
81
|
const PAGINATION_PARAM_NAMES = new Set(['limit', 'before', 'after', 'order']);
|
|
65
82
|
|
|
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
|
-
};
|
|
83
|
+
/** Map HTTP status codes to their corresponding exception class names for @throws docs.
|
|
84
|
+
* Built from sdk.errors.statusCodeMap with Node-specific naming overrides. */
|
|
85
|
+
const STATUS_TO_EXCEPTION_NAME: Record<number, string> = buildNodeStatusExceptions();
|
|
75
86
|
|
|
76
87
|
/**
|
|
77
88
|
* Compute the options interface name for a paginated method.
|
|
@@ -92,18 +103,9 @@ function httpMethodNeedsBody(method: string): boolean {
|
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
// ---------------------------------------------------------------------------
|
|
95
|
-
// Method-name
|
|
106
|
+
// Method-name deduplication helpers
|
|
96
107
|
// ---------------------------------------------------------------------------
|
|
97
108
|
|
|
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
109
|
/** Split a camelCase/PascalCase name into lowercase word parts. */
|
|
108
110
|
function splitCamelWords(name: string): string[] {
|
|
109
111
|
const parts: string[] = [];
|
|
@@ -123,209 +125,6 @@ function singularize(word: string): string {
|
|
|
123
125
|
return word.endsWith('s') && !word.endsWith('ss') ? word.slice(0, -1) : word;
|
|
124
126
|
}
|
|
125
127
|
|
|
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
128
|
/**
|
|
330
129
|
* Deduplicate method names within the plans array.
|
|
331
130
|
*
|
|
@@ -397,7 +196,13 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
397
196
|
if (services.length === 0) return [];
|
|
398
197
|
const files: GeneratedFile[] = [];
|
|
399
198
|
|
|
400
|
-
|
|
199
|
+
// Group services by mount target to avoid file path collisions when
|
|
200
|
+
// multiple IR services mount to the same resource class.
|
|
201
|
+
const mountGroups = groupByMount(ctx);
|
|
202
|
+
const mergedServices: Service[] =
|
|
203
|
+
mountGroups.size > 0 ? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations })) : services;
|
|
204
|
+
|
|
205
|
+
for (const service of mergedServices) {
|
|
401
206
|
if (isServiceCoveredByExisting(service, ctx)) {
|
|
402
207
|
// Fully covered: generate with ALL operations so the merger's docstring
|
|
403
208
|
// refresh pass can update JSDoc on existing methods.
|
|
@@ -406,6 +211,9 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
406
211
|
// remove skipIfExists so the merger adds the new methods.
|
|
407
212
|
if (hasMethodsAbsentFromBaseline(service, ctx)) {
|
|
408
213
|
delete file.skipIfExists;
|
|
214
|
+
// Suppress auto-generated header — the file is a merge target
|
|
215
|
+
// containing hand-written code, not a fully generated file.
|
|
216
|
+
file.headerPlacement = 'skip';
|
|
409
217
|
}
|
|
410
218
|
files.push(file);
|
|
411
219
|
continue;
|
|
@@ -420,6 +228,9 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
420
228
|
// merger adds new methods AND refreshes existing JSDoc.
|
|
421
229
|
const file = generateResourceClass(service, ctx);
|
|
422
230
|
delete file.skipIfExists;
|
|
231
|
+
// Suppress auto-generated header — the file is a merge target
|
|
232
|
+
// containing hand-written code, not a fully generated file.
|
|
233
|
+
file.headerPlacement = 'skip';
|
|
423
234
|
files.push(file);
|
|
424
235
|
} else {
|
|
425
236
|
files.push(generateResourceClass(service, ctx));
|
|
@@ -441,10 +252,8 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
441
252
|
method: resolveMethodName(op, service, ctx),
|
|
442
253
|
}));
|
|
443
254
|
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
// spec-derived name doesn't match the hand-written SDK name.
|
|
447
|
-
reconcileMethodNames(plans, service, ctx);
|
|
255
|
+
// Resolved operations already produce correct method names via the
|
|
256
|
+
// centralized hint map — no per-emitter reconciliation needed.
|
|
448
257
|
|
|
449
258
|
// Deduplicate method names within the class (e.g., two operations both
|
|
450
259
|
// resolving to "create" for different paths).
|
|
@@ -458,18 +267,23 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
458
267
|
// order, JSDoc comments get attached to the wrong methods (list↔create,
|
|
459
268
|
// add↔set swaps). Sorting by the overlay's method order ensures the
|
|
460
269
|
// generated output matches the existing file's method order.
|
|
270
|
+
//
|
|
271
|
+
// We build the order from HTTP operation keys (e.g., "GET /organizations")
|
|
272
|
+
// rather than method names, because resolveMethodName may return a different
|
|
273
|
+
// name than the overlay's methodName (e.g., when the hint map overrides it),
|
|
274
|
+
// causing the lookup to fail and the sort to produce wrong order.
|
|
461
275
|
if (ctx.overlayLookup?.methodByOperation) {
|
|
462
|
-
const
|
|
276
|
+
const httpKeyOrder = new Map<string, number>();
|
|
463
277
|
let pos = 0;
|
|
464
|
-
for (const [
|
|
465
|
-
|
|
466
|
-
methodOrder.set(info.methodName, pos++);
|
|
467
|
-
}
|
|
278
|
+
for (const [httpKey] of ctx.overlayLookup.methodByOperation) {
|
|
279
|
+
httpKeyOrder.set(httpKey, pos++);
|
|
468
280
|
}
|
|
469
|
-
if (
|
|
281
|
+
if (httpKeyOrder.size > 0) {
|
|
470
282
|
plans.sort((a, b) => {
|
|
471
|
-
const
|
|
472
|
-
const
|
|
283
|
+
const aKey = `${a.op.httpMethod.toUpperCase()} ${a.op.path}`;
|
|
284
|
+
const bKey = `${b.op.httpMethod.toUpperCase()} ${b.op.path}`;
|
|
285
|
+
const aPos = httpKeyOrder.get(aKey) ?? Number.MAX_SAFE_INTEGER;
|
|
286
|
+
const bPos = httpKeyOrder.get(bKey) ?? Number.MAX_SAFE_INTEGER;
|
|
473
287
|
return aPos - bPos;
|
|
474
288
|
});
|
|
475
289
|
}
|
|
@@ -478,13 +292,32 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
478
292
|
const hasPaginated = plans.some((p) => p.plan.isPaginated);
|
|
479
293
|
const modelMap = new Map(ctx.spec.models.map((m) => [m.name, m]));
|
|
480
294
|
|
|
295
|
+
// When merging into an existing class, the merger keeps baseline method
|
|
296
|
+
// bodies but may add imports from the generated code. To avoid orphaned
|
|
297
|
+
// imports for types used only by baseline methods (whose bodies are kept
|
|
298
|
+
// intact), skip model collection for methods that already exist.
|
|
299
|
+
const baselineMethodSet = new Set<string>();
|
|
300
|
+
const baselineClass = ctx.apiSurface?.classes?.[serviceClass];
|
|
301
|
+
if (baselineClass?.methods) {
|
|
302
|
+
for (const name of Object.keys(baselineClass.methods)) {
|
|
303
|
+
baselineMethodSet.add(name);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
481
307
|
// Collect models for imports — only include models that are actually used
|
|
482
308
|
// in method signatures (not all union variants from the spec)
|
|
483
309
|
const responseModels = new Set<string>();
|
|
484
310
|
const requestModels = new Set<string>();
|
|
485
311
|
const paramEnums = new Set<string>();
|
|
486
312
|
const paramModels = new Set<string>();
|
|
487
|
-
for (const { op, plan } of plans) {
|
|
313
|
+
for (const { op, plan, method } of plans) {
|
|
314
|
+
// Skip imports for methods that already exist in the baseline class.
|
|
315
|
+
// The merger keeps baseline method bodies, so their imports are already
|
|
316
|
+
// present in the existing file. Including them here would create
|
|
317
|
+
// orphaned imports when the generated return type differs from the
|
|
318
|
+
// baseline's (e.g., generated `List` vs baseline `RoleList`).
|
|
319
|
+
if (baselineMethodSet.has(method)) continue;
|
|
320
|
+
|
|
488
321
|
if (plan.isPaginated && op.pagination?.itemType.kind === 'model') {
|
|
489
322
|
// For paginated operations, import the item type (e.g., Connection)
|
|
490
323
|
// rather than the list wrapper type (e.g., ConnectionList).
|
|
@@ -531,6 +364,20 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
531
364
|
collectParamTypeRefs(param.type, paramEnums, paramModels);
|
|
532
365
|
}
|
|
533
366
|
}
|
|
367
|
+
|
|
368
|
+
// Collect response models from union split wrappers so their types and
|
|
369
|
+
// deserializers are imported alongside the primary operation models.
|
|
370
|
+
const resolvedLookup = buildResolvedLookup(ctx);
|
|
371
|
+
for (const { op, method } of plans) {
|
|
372
|
+
if (baselineMethodSet.has(method)) continue;
|
|
373
|
+
const resolved = lookupResolved(op, resolvedLookup);
|
|
374
|
+
if (resolved) {
|
|
375
|
+
for (const name of collectWrapperResponseModels(resolved)) {
|
|
376
|
+
responseModels.add(name);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
534
381
|
const allModels = new Set([...responseModels, ...requestModels, ...paramModels]);
|
|
535
382
|
|
|
536
383
|
const lines: string[] = [];
|
|
@@ -676,20 +523,29 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
676
523
|
} else if (!plan.isPaginated && !plan.hasBody && !plan.isDelete && op.queryParams.length > 0) {
|
|
677
524
|
// Non-paginated GET or void methods with query params get a typed options interface
|
|
678
525
|
// instead of falling back to Record<string, unknown>.
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
526
|
+
// Filter out hidden params (defaults and inferFromClient fields)
|
|
527
|
+
const resolved = lookupResolved(op, resolvedLookup);
|
|
528
|
+
const opHiddenParams = new Set<string>([
|
|
529
|
+
...Object.keys(getOpDefaults(resolved)),
|
|
530
|
+
...getOpInferFromClient(resolved),
|
|
531
|
+
]);
|
|
532
|
+
const visibleParams = op.queryParams.filter((p) => !opHiddenParams.has(p.name));
|
|
533
|
+
if (visibleParams.length > 0) {
|
|
534
|
+
const optionsName = toPascalCase(method) + 'Options';
|
|
535
|
+
lines.push(`export interface ${optionsName} {`);
|
|
536
|
+
for (const param of visibleParams) {
|
|
537
|
+
const opt = !param.required ? '?' : '';
|
|
538
|
+
if (param.description || param.deprecated) {
|
|
539
|
+
const parts: string[] = [];
|
|
540
|
+
if (param.description) parts.push(param.description);
|
|
541
|
+
if (param.deprecated) parts.push('@deprecated');
|
|
542
|
+
lines.push(...docComment(parts.join('\n'), 2));
|
|
543
|
+
}
|
|
544
|
+
lines.push(` ${fieldName(param.name)}${opt}: ${mapParamType(param.type, specEnumNames)};`);
|
|
688
545
|
}
|
|
689
|
-
lines.push(
|
|
546
|
+
lines.push('}');
|
|
547
|
+
lines.push('');
|
|
690
548
|
}
|
|
691
|
-
lines.push('}');
|
|
692
|
-
lines.push('');
|
|
693
549
|
}
|
|
694
550
|
}
|
|
695
551
|
|
|
@@ -702,7 +558,13 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
|
|
|
702
558
|
|
|
703
559
|
for (const { op, plan, method } of plans) {
|
|
704
560
|
lines.push('');
|
|
705
|
-
|
|
561
|
+
const resolved = lookupResolved(op, resolvedLookup);
|
|
562
|
+
lines.push(...renderMethod(op, plan, method, service, ctx, modelMap, specEnumNames, resolved));
|
|
563
|
+
|
|
564
|
+
// Emit union split wrapper methods (typed convenience methods for each variant)
|
|
565
|
+
if (resolved?.wrappers && resolved.wrappers.length > 0) {
|
|
566
|
+
lines.push(...generateWrapperMethods(resolved, ctx));
|
|
567
|
+
}
|
|
706
568
|
}
|
|
707
569
|
|
|
708
570
|
lines.push('}');
|
|
@@ -718,12 +580,20 @@ function renderMethod(
|
|
|
718
580
|
ctx: EmitterContext,
|
|
719
581
|
modelMap: Map<string, Model>,
|
|
720
582
|
specEnumNames?: Set<string>,
|
|
583
|
+
resolvedOp?: ResolvedOperation,
|
|
721
584
|
): string[] {
|
|
722
585
|
const lines: string[] = [];
|
|
723
586
|
const responseModel = plan.responseModelName ? resolveInterfaceName(plan.responseModelName, ctx) : null;
|
|
724
587
|
|
|
725
588
|
const pathStr = buildPathStr(op);
|
|
726
589
|
|
|
590
|
+
// Build the set of params hidden from the method signature
|
|
591
|
+
// (injected from client config or as constant defaults)
|
|
592
|
+
const hiddenParams = new Set<string>([
|
|
593
|
+
...Object.keys(getOpDefaults(resolvedOp)),
|
|
594
|
+
...getOpInferFromClient(resolvedOp),
|
|
595
|
+
]);
|
|
596
|
+
|
|
727
597
|
// Build set of valid param names to filter @param tags.
|
|
728
598
|
// Prefer the overlay (existing method signature) if available;
|
|
729
599
|
// otherwise compute from what the render path will actually include.
|
|
@@ -739,119 +609,135 @@ function renderMethod(
|
|
|
739
609
|
for (const p of op.pathParams) actualParams.add(fieldName(p.name));
|
|
740
610
|
if (plan.hasBody) actualParams.add('payload');
|
|
741
611
|
if (plan.isPaginated) actualParams.add('options');
|
|
742
|
-
// renderGetMethod
|
|
743
|
-
|
|
612
|
+
// renderGetMethod/renderVoidMethod add options when there are visible non-paginated query params
|
|
613
|
+
const visibleQueryCount = op.queryParams.filter((q) => !hiddenParams.has(q.name)).length;
|
|
614
|
+
if (!plan.isPaginated && visibleQueryCount > 0 && !plan.isDelete) {
|
|
744
615
|
actualParams.add('options');
|
|
745
616
|
}
|
|
746
617
|
validParamNames = actualParams;
|
|
747
618
|
}
|
|
748
619
|
|
|
749
|
-
|
|
750
|
-
if
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
620
|
+
// Always generate JSDoc for all methods (both existing and new).
|
|
621
|
+
// The merger matches docstrings by member name — if we skip JSDoc for
|
|
622
|
+
// existing methods, previously misplaced docstrings can never be corrected.
|
|
623
|
+
// Hand-written docs (e.g., @deprecated, PKCE flow descriptions) are
|
|
624
|
+
// preserved by the merger's @deprecated-preservation and @oagen-ignore
|
|
625
|
+
// mechanisms instead.
|
|
626
|
+
{
|
|
627
|
+
const docParts: string[] = [];
|
|
628
|
+
if (op.description) docParts.push(op.description);
|
|
629
|
+
for (const param of op.pathParams) {
|
|
630
|
+
const paramName = fieldName(param.name);
|
|
631
|
+
if (validParamNames && !validParamNames.has(paramName)) continue;
|
|
632
|
+
const deprecatedPrefix = param.deprecated ? '(deprecated) ' : '';
|
|
633
|
+
if (param.description) {
|
|
634
|
+
docParts.push(`@param ${paramName} - ${deprecatedPrefix}${param.description}`);
|
|
635
|
+
} else if (param.deprecated) {
|
|
636
|
+
docParts.push(`@param ${paramName} - (deprecated)`);
|
|
637
|
+
}
|
|
638
|
+
if (param.default !== undefined) docParts.push(`@default ${JSON.stringify(param.default)}`);
|
|
639
|
+
if (param.example !== undefined) docParts.push(`@example ${JSON.stringify(param.example)}`);
|
|
759
640
|
}
|
|
760
|
-
|
|
761
|
-
if (
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
641
|
+
// Document query params for non-paginated operations
|
|
642
|
+
if (!plan.isPaginated) {
|
|
643
|
+
// Only document query params if the method will have an options parameter
|
|
644
|
+
if (validParamNames && (validParamNames.has('options') || overlayMethod)) {
|
|
645
|
+
for (const param of op.queryParams) {
|
|
646
|
+
if (hiddenParams.has(param.name)) continue;
|
|
647
|
+
const paramName = `options.${fieldName(param.name)}`;
|
|
648
|
+
if (validParamNames && !validParamNames.has('options') && !validParamNames.has(fieldName(param.name)))
|
|
649
|
+
continue;
|
|
650
|
+
const deprecatedPrefix = param.deprecated ? '(deprecated) ' : '';
|
|
651
|
+
if (param.description) {
|
|
652
|
+
docParts.push(`@param ${paramName} - ${deprecatedPrefix}${param.description}`);
|
|
653
|
+
} else if (param.deprecated) {
|
|
654
|
+
docParts.push(`@param ${paramName} - (deprecated)`);
|
|
655
|
+
}
|
|
656
|
+
if (param.default !== undefined) docParts.push(`@default ${JSON.stringify(param.default)}`);
|
|
657
|
+
if (param.example !== undefined) docParts.push(`@example ${JSON.stringify(param.example)}`);
|
|
775
658
|
}
|
|
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
659
|
}
|
|
779
660
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
661
|
+
// Skip header and cookie params in JSDoc — they are not exposed in the method signature.
|
|
662
|
+
// The SDK handles headers and cookies internally, so documenting them would be misleading.
|
|
663
|
+
// Document payload parameter when there is a request body
|
|
664
|
+
if (plan.hasBody) {
|
|
665
|
+
const bodyInfo = extractRequestBodyType(op, ctx);
|
|
666
|
+
if (bodyInfo?.kind === 'model') {
|
|
667
|
+
const bodyModel = ctx.spec.models.find((m) => m.name === bodyInfo.name);
|
|
668
|
+
let payloadDesc: string;
|
|
669
|
+
if (bodyModel?.description) {
|
|
670
|
+
payloadDesc = `@param payload - ${bodyModel.description}`;
|
|
671
|
+
} else if (bodyModel) {
|
|
672
|
+
// When the model lacks a description, list its required fields to help
|
|
673
|
+
// callers understand what must be provided.
|
|
674
|
+
const requiredFieldNames = bodyModel.fields.filter((f) => f.required).map((f) => fieldName(f.name));
|
|
675
|
+
payloadDesc =
|
|
676
|
+
requiredFieldNames.length > 0
|
|
677
|
+
? `@param payload - Object containing ${requiredFieldNames.join(', ')}.`
|
|
678
|
+
: '@param payload - The request body.';
|
|
679
|
+
} else {
|
|
680
|
+
payloadDesc = '@param payload - The request body.';
|
|
681
|
+
}
|
|
682
|
+
docParts.push(payloadDesc);
|
|
799
683
|
} else {
|
|
800
|
-
|
|
684
|
+
docParts.push('@param payload - The request body.');
|
|
801
685
|
}
|
|
802
|
-
docParts.push(payloadDesc);
|
|
803
|
-
} else {
|
|
804
|
-
docParts.push('@param payload - The request body.');
|
|
805
686
|
}
|
|
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;
|
|
687
|
+
// Document options parameter for paginated operations
|
|
688
|
+
if (plan.isPaginated) {
|
|
689
|
+
docParts.push('@param options - Pagination and filter options.');
|
|
690
|
+
} else if (op.queryParams.filter((q) => !hiddenParams.has(q.name)).length > 0) {
|
|
691
|
+
docParts.push('@param options - Additional query options.');
|
|
822
692
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
693
|
+
// @returns for the primary response model.
|
|
694
|
+
// When an overlay method exists, prefer its return type so the JSDoc
|
|
695
|
+
// matches the actual TypeScript signature (the overlay may use a
|
|
696
|
+
// different model name than the OpenAPI schema).
|
|
697
|
+
if (overlayMethod?.returnType) {
|
|
698
|
+
docParts.push(`@returns {${overlayMethod.returnType}}`);
|
|
699
|
+
} else if (plan.isPaginated && op.pagination?.itemType.kind === 'model') {
|
|
700
|
+
// Unwrap list wrapper models to match the actual return type — the method returns
|
|
701
|
+
// AutoPaginatable<ItemType>, not the list wrapper.
|
|
702
|
+
let itemRawName = op.pagination.itemType.name;
|
|
703
|
+
const pModel = modelMap.get(itemRawName);
|
|
704
|
+
if (pModel) {
|
|
705
|
+
const unwrapped = unwrapListModel(pModel, modelMap);
|
|
706
|
+
if (unwrapped) itemRawName = unwrapped.name;
|
|
707
|
+
}
|
|
708
|
+
const itemTypeName = resolveInterfaceName(itemRawName, ctx);
|
|
709
|
+
docParts.push(`@returns {Promise<AutoPaginatable<${itemTypeName}>>}`);
|
|
710
|
+
} else if (responseModel) {
|
|
711
|
+
docParts.push(`@returns {Promise<${responseModel}>}`);
|
|
712
|
+
} else {
|
|
713
|
+
docParts.push('@returns {Promise<void>}');
|
|
835
714
|
}
|
|
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);
|
|
715
|
+
// @throws for error responses
|
|
716
|
+
for (const err of op.errors) {
|
|
717
|
+
const exceptionName = STATUS_TO_EXCEPTION_NAME[err.statusCode];
|
|
718
|
+
if (exceptionName) {
|
|
719
|
+
docParts.push(`@throws {${exceptionName}} ${err.statusCode}`);
|
|
845
720
|
}
|
|
846
721
|
}
|
|
847
|
-
if (
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
lines
|
|
851
|
-
|
|
852
|
-
|
|
722
|
+
if (op.deprecated) docParts.push('@deprecated');
|
|
723
|
+
|
|
724
|
+
if (docParts.length > 0) {
|
|
725
|
+
// Flatten all parts, splitting multiline descriptions into individual lines
|
|
726
|
+
const allLines: string[] = [];
|
|
727
|
+
for (const part of docParts) {
|
|
728
|
+
for (const line of part.split('\n')) {
|
|
729
|
+
allLines.push(line);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (allLines.length === 1) {
|
|
733
|
+
lines.push(` /** ${allLines[0]} */`);
|
|
734
|
+
} else {
|
|
735
|
+
lines.push(' /**');
|
|
736
|
+
for (const line of allLines) {
|
|
737
|
+
lines.push(line === '' ? ' *' : ` * ${line}`);
|
|
738
|
+
}
|
|
739
|
+
lines.push(' */');
|
|
853
740
|
}
|
|
854
|
-
lines.push(' */');
|
|
855
741
|
}
|
|
856
742
|
}
|
|
857
743
|
|
|
@@ -895,9 +781,9 @@ function renderMethod(
|
|
|
895
781
|
} else if (plan.hasBody && responseModel) {
|
|
896
782
|
renderBodyMethod(lines, op, plan, method, responseModel, pathStr, ctx, specEnumNames);
|
|
897
783
|
} else if (responseModel) {
|
|
898
|
-
renderGetMethod(lines, op, plan, method, responseModel, pathStr, specEnumNames);
|
|
784
|
+
renderGetMethod(lines, op, plan, method, responseModel, pathStr, specEnumNames, resolvedOp);
|
|
899
785
|
} else {
|
|
900
|
-
renderVoidMethod(lines, op, plan, method, pathStr, ctx, specEnumNames);
|
|
786
|
+
renderVoidMethod(lines, op, plan, method, pathStr, ctx, specEnumNames, resolvedOp);
|
|
901
787
|
}
|
|
902
788
|
|
|
903
789
|
// Defensive: if no render function produced a method body, emit a stub
|
|
@@ -1081,21 +967,78 @@ function renderGetMethod(
|
|
|
1081
967
|
responseModel: string,
|
|
1082
968
|
pathStr: string,
|
|
1083
969
|
specEnumNames?: Set<string>,
|
|
970
|
+
resolvedOp?: ResolvedOperation,
|
|
1084
971
|
): void {
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
972
|
+
const hiddenParams = new Set<string>([
|
|
973
|
+
...Object.keys(getOpDefaults(resolvedOp)),
|
|
974
|
+
...getOpInferFromClient(resolvedOp),
|
|
975
|
+
]);
|
|
1088
976
|
|
|
1089
|
-
const
|
|
977
|
+
const params = buildPathParams(op, specEnumNames);
|
|
978
|
+
const visibleQueryParams = op.queryParams.filter((p) => !hiddenParams.has(p.name));
|
|
979
|
+
const hasVisibleQuery = visibleQueryParams.length > 0 && !plan.isPaginated;
|
|
980
|
+
const hasDefaults = Object.keys(getOpDefaults(resolvedOp)).length > 0;
|
|
981
|
+
const hasInferred = getOpInferFromClient(resolvedOp).length > 0;
|
|
982
|
+
const hasInjected = hasDefaults || hasInferred;
|
|
983
|
+
const hasQuery = (op.queryParams.length > 0 && !plan.isPaginated) || hasInjected;
|
|
984
|
+
const optionsType = hasVisibleQuery ? toPascalCase(method) + 'Options' : null;
|
|
985
|
+
|
|
986
|
+
const allParams = optionsType
|
|
987
|
+
? params
|
|
988
|
+
? `${params}, options?: ${optionsType}`
|
|
989
|
+
: `options?: ${optionsType}`
|
|
990
|
+
: params;
|
|
1090
991
|
|
|
1091
992
|
lines.push(` async ${method}(${allParams}): Promise<${responseModel}> {`);
|
|
1092
993
|
if (hasQuery) {
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
994
|
+
if (hasInjected) {
|
|
995
|
+
// Build the query object with visible params, defaults, and inferred fields
|
|
996
|
+
const queryParts: string[] = [];
|
|
997
|
+
|
|
998
|
+
// Regular visible query params (from options)
|
|
999
|
+
if (hasVisibleQuery) {
|
|
1000
|
+
for (const param of visibleQueryParams) {
|
|
1001
|
+
const camel = fieldName(param.name);
|
|
1002
|
+
const snake = wireFieldName(param.name);
|
|
1003
|
+
if (camel === snake) {
|
|
1004
|
+
if (param.required) {
|
|
1005
|
+
queryParts.push(`${camel}: options.${camel}`);
|
|
1006
|
+
} else {
|
|
1007
|
+
queryParts.push(`...(options?.${camel} !== undefined && { ${camel}: options.${camel} })`);
|
|
1008
|
+
}
|
|
1009
|
+
} else {
|
|
1010
|
+
if (param.required) {
|
|
1011
|
+
queryParts.push(`${snake}: options.${camel}`);
|
|
1012
|
+
} else {
|
|
1013
|
+
queryParts.push(`...(options?.${camel} !== undefined && { ${snake}: options.${camel} })`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Constant defaults (e.g., response_type: 'code')
|
|
1020
|
+
for (const [key, value] of Object.entries(getOpDefaults(resolvedOp))) {
|
|
1021
|
+
queryParts.push(`${key}: ${tsLiteral(value)}`);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Inferred fields from client config (e.g., client_id from this.workos.options.clientId)
|
|
1025
|
+
for (const field of getOpInferFromClient(resolvedOp)) {
|
|
1026
|
+
queryParts.push(`${field}: ${clientFieldExpression(field)}`);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
lines.push(
|
|
1030
|
+
` const { data } = await this.workos.${op.httpMethod}<${wireInterfaceName(responseModel)}>(${pathStr}, {`,
|
|
1031
|
+
);
|
|
1032
|
+
lines.push(` query: { ${queryParts.join(', ')} },`);
|
|
1033
|
+
lines.push(' });');
|
|
1034
|
+
} else {
|
|
1035
|
+
const queryExpr = renderQueryExpr(visibleQueryParams);
|
|
1036
|
+
lines.push(
|
|
1037
|
+
` const { data } = await this.workos.${op.httpMethod}<${wireInterfaceName(responseModel)}>(${pathStr}, {`,
|
|
1038
|
+
);
|
|
1039
|
+
lines.push(` query: ${queryExpr},`);
|
|
1040
|
+
lines.push(' });');
|
|
1041
|
+
}
|
|
1099
1042
|
} else if (httpMethodNeedsBody(op.httpMethod)) {
|
|
1100
1043
|
// PUT/PATCH/POST require a body argument even when the spec has no request body
|
|
1101
1044
|
lines.push(
|
|
@@ -1110,6 +1053,25 @@ function renderGetMethod(
|
|
|
1110
1053
|
lines.push(' }');
|
|
1111
1054
|
}
|
|
1112
1055
|
|
|
1056
|
+
/** Convert a JS value to a TypeScript literal. */
|
|
1057
|
+
function tsLiteral(value: string | number | boolean): string {
|
|
1058
|
+
if (typeof value === 'string') return `'${value.replace(/'/g, "\\'")}'`;
|
|
1059
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
1060
|
+
return String(value);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/** Get the TypeScript expression for reading a client config field. */
|
|
1064
|
+
function clientFieldExpression(field: string): string {
|
|
1065
|
+
switch (field) {
|
|
1066
|
+
case 'client_id':
|
|
1067
|
+
return 'this.workos.options.clientId';
|
|
1068
|
+
case 'client_secret':
|
|
1069
|
+
return 'this.workos.key';
|
|
1070
|
+
default:
|
|
1071
|
+
return `this.workos.${toCamelCase(field)}`;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1113
1075
|
function renderVoidMethod(
|
|
1114
1076
|
lines: string[],
|
|
1115
1077
|
op: Operation,
|
|
@@ -1118,10 +1080,21 @@ function renderVoidMethod(
|
|
|
1118
1080
|
pathStr: string,
|
|
1119
1081
|
ctx: EmitterContext,
|
|
1120
1082
|
specEnumNames?: Set<string>,
|
|
1083
|
+
resolvedOp?: ResolvedOperation,
|
|
1121
1084
|
): void {
|
|
1085
|
+
const hiddenParams = new Set<string>([
|
|
1086
|
+
...Object.keys(getOpDefaults(resolvedOp)),
|
|
1087
|
+
...getOpInferFromClient(resolvedOp),
|
|
1088
|
+
]);
|
|
1089
|
+
|
|
1122
1090
|
const params = buildPathParams(op, specEnumNames);
|
|
1123
|
-
const
|
|
1124
|
-
const
|
|
1091
|
+
const visibleQueryParams = op.queryParams.filter((p) => !hiddenParams.has(p.name));
|
|
1092
|
+
const hasVisibleQuery = visibleQueryParams.length > 0 && !plan.hasBody;
|
|
1093
|
+
const hasDefaults = Object.keys(getOpDefaults(resolvedOp)).length > 0;
|
|
1094
|
+
const hasInferred = getOpInferFromClient(resolvedOp).length > 0;
|
|
1095
|
+
const hasInjected = hasDefaults || hasInferred;
|
|
1096
|
+
const hasQuery = hasVisibleQuery || (hasInjected && !plan.hasBody);
|
|
1097
|
+
const optionsType = hasVisibleQuery ? toPascalCase(method) + 'Options' : null;
|
|
1125
1098
|
|
|
1126
1099
|
let bodyParam = '';
|
|
1127
1100
|
let bodyExpr = 'payload';
|
|
@@ -1154,10 +1127,47 @@ function renderVoidMethod(
|
|
|
1154
1127
|
if (plan.hasBody) {
|
|
1155
1128
|
lines.push(` await this.workos.${op.httpMethod}(${pathStr}, ${bodyExpr});`);
|
|
1156
1129
|
} else if (hasQuery) {
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1130
|
+
if (hasInjected) {
|
|
1131
|
+
// Build query object with visible params, defaults, and inferred fields
|
|
1132
|
+
const queryParts: string[] = [];
|
|
1133
|
+
|
|
1134
|
+
if (hasVisibleQuery) {
|
|
1135
|
+
for (const param of visibleQueryParams) {
|
|
1136
|
+
const camel = fieldName(param.name);
|
|
1137
|
+
const snake = wireFieldName(param.name);
|
|
1138
|
+
if (camel === snake) {
|
|
1139
|
+
if (param.required) {
|
|
1140
|
+
queryParts.push(`${camel}: options.${camel}`);
|
|
1141
|
+
} else {
|
|
1142
|
+
queryParts.push(`...(options?.${camel} !== undefined && { ${camel}: options.${camel} })`);
|
|
1143
|
+
}
|
|
1144
|
+
} else {
|
|
1145
|
+
if (param.required) {
|
|
1146
|
+
queryParts.push(`${snake}: options.${camel}`);
|
|
1147
|
+
} else {
|
|
1148
|
+
queryParts.push(`...(options?.${camel} !== undefined && { ${snake}: options.${camel} })`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
for (const [key, value] of Object.entries(getOpDefaults(resolvedOp))) {
|
|
1155
|
+
queryParts.push(`${key}: ${tsLiteral(value)}`);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
for (const field of getOpInferFromClient(resolvedOp)) {
|
|
1159
|
+
queryParts.push(`${field}: ${clientFieldExpression(field)}`);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
lines.push(` await this.workos.${op.httpMethod}(${pathStr}, {`);
|
|
1163
|
+
lines.push(` query: { ${queryParts.join(', ')} },`);
|
|
1164
|
+
lines.push(' });');
|
|
1165
|
+
} else {
|
|
1166
|
+
const queryExpr = renderQueryExpr(visibleQueryParams);
|
|
1167
|
+
lines.push(` await this.workos.${op.httpMethod}(${pathStr}, {`);
|
|
1168
|
+
lines.push(` query: ${queryExpr},`);
|
|
1169
|
+
lines.push(' });');
|
|
1170
|
+
}
|
|
1161
1171
|
} else if (httpMethodNeedsBody(op.httpMethod)) {
|
|
1162
1172
|
lines.push(` await this.workos.${op.httpMethod}(${pathStr}, {});`);
|
|
1163
1173
|
} else {
|