@workos/oagen-emitters 0.14.3 → 0.15.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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +19 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{plugin-D0qLBiGv.mjs → plugin-CO4RFgAW.mjs} +983 -270
- package/dist/plugin-CO4RFgAW.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +9 -9
- package/renovate.json +1 -61
- package/src/go/client.ts +1 -1
- package/src/go/enums.ts +77 -0
- package/src/kotlin/enums.ts +11 -4
- package/src/node/client.ts +119 -2
- package/src/node/discriminated-models.ts +20 -28
- package/src/node/field-plan.ts +64 -8
- package/src/node/index.ts +59 -3
- package/src/node/models.ts +73 -30
- package/src/node/naming.ts +14 -1
- package/src/node/node-overrides.ts +4 -37
- package/src/node/options.ts +29 -1
- package/src/node/resources.ts +533 -83
- package/src/node/tests.ts +108 -7
- package/src/php/fixtures.ts +4 -1
- package/src/php/models.ts +3 -1
- package/src/php/resources.ts +40 -11
- package/src/php/tests.ts +22 -12
- package/src/python/client.ts +0 -8
- package/src/python/enums.ts +41 -15
- package/src/python/fixtures.ts +23 -7
- package/src/python/models.ts +26 -5
- package/src/python/resources.ts +71 -3
- package/src/python/tests.ts +70 -12
- package/src/python/wrappers.ts +25 -4
- package/src/ruby/client.ts +0 -1
- package/src/ruby/rbi.ts +12 -6
- package/src/rust/resources.ts +10 -7
- package/src/shared/non-spec-services.ts +0 -5
- package/test/go/enums.test.ts +24 -0
- package/test/node/resources.test.ts +11 -1
- package/test/node/tests.test.ts +3 -3
- package/test/php/client.test.ts +0 -1
- package/test/php/resources.test.ts +50 -0
- package/test/rust/resources.test.ts +9 -0
- package/dist/plugin-D0qLBiGv.mjs.map +0 -1
package/dist/plugin.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as workosEmittersPlugin } from "./plugin-
|
|
1
|
+
import { t as workosEmittersPlugin } from "./plugin-CO4RFgAW.mjs";
|
|
2
2
|
export { workosEmittersPlugin };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workos/oagen-emitters",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "WorkOS' oagen emitters",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "WorkOS",
|
|
@@ -38,22 +38,22 @@
|
|
|
38
38
|
"prepare": "husky"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@commitlint/cli": "^21.0.
|
|
42
|
-
"@commitlint/config-conventional": "^21.0.
|
|
41
|
+
"@commitlint/cli": "^21.0.2",
|
|
42
|
+
"@commitlint/config-conventional": "^21.0.2",
|
|
43
43
|
"@types/node": "^25.9.1",
|
|
44
44
|
"husky": "^9.1.7",
|
|
45
|
-
"oxfmt": "^0.
|
|
46
|
-
"oxlint": "^1.
|
|
45
|
+
"oxfmt": "^0.52.0",
|
|
46
|
+
"oxlint": "^1.67.0",
|
|
47
47
|
"prettier": "^3.8.3",
|
|
48
|
-
"tsdown": "^0.22.
|
|
49
|
-
"tsx": "^4.22.
|
|
48
|
+
"tsdown": "^0.22.1",
|
|
49
|
+
"tsx": "^4.22.4",
|
|
50
50
|
"typescript": "^6.0.3",
|
|
51
|
-
"vitest": "^4.1.
|
|
51
|
+
"vitest": "^4.1.8"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=24.10.0"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@workos/oagen": "^0.
|
|
57
|
+
"@workos/oagen": "^0.21.1"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/renovate.json
CHANGED
|
@@ -1,66 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
3
3
|
"extends": [
|
|
4
|
-
"github>workos/renovate-config"
|
|
5
|
-
],
|
|
6
|
-
"dependencyDashboard": false,
|
|
7
|
-
"schedule": [
|
|
8
|
-
"on the 15th day of the month before 12pm"
|
|
9
|
-
],
|
|
10
|
-
"timezone": "UTC",
|
|
11
|
-
"rebaseWhen": "conflicted",
|
|
12
|
-
"packageRules": [
|
|
13
|
-
{
|
|
14
|
-
"matchManagers": [
|
|
15
|
-
"github-actions"
|
|
16
|
-
],
|
|
17
|
-
"pinDigests": true,
|
|
18
|
-
"extractVersion": "^v(?<version>\\d+\\.\\d+\\.\\d+)$"
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"matchUpdateTypes": [
|
|
22
|
-
"minor",
|
|
23
|
-
"patch"
|
|
24
|
-
],
|
|
25
|
-
"automerge": true,
|
|
26
|
-
"groupName": "minor and patch updates"
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
"matchUpdateTypes": [
|
|
30
|
-
"major"
|
|
31
|
-
],
|
|
32
|
-
"automerge": false
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
"matchUpdateTypes": [
|
|
36
|
-
"digest"
|
|
37
|
-
],
|
|
38
|
-
"automerge": false
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
"matchManagers": [
|
|
42
|
-
"github-actions"
|
|
43
|
-
],
|
|
44
|
-
"matchUpdateTypes": [
|
|
45
|
-
"minor",
|
|
46
|
-
"patch",
|
|
47
|
-
"digest",
|
|
48
|
-
"pinDigest"
|
|
49
|
-
],
|
|
50
|
-
"groupName": "github actions non-major",
|
|
51
|
-
"groupSlug": "github-actions-non-major",
|
|
52
|
-
"automerge": true
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"matchManagers": [
|
|
56
|
-
"github-actions"
|
|
57
|
-
],
|
|
58
|
-
"matchUpdateTypes": [
|
|
59
|
-
"major"
|
|
60
|
-
],
|
|
61
|
-
"groupName": "github actions major",
|
|
62
|
-
"groupSlug": "github-actions-major",
|
|
63
|
-
"automerge": false
|
|
64
|
-
}
|
|
4
|
+
"github>workos/renovate-config:public"
|
|
65
5
|
]
|
|
66
6
|
}
|
package/src/go/client.ts
CHANGED
|
@@ -17,7 +17,7 @@ export function generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFil
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Non-spec services marked with `hasClientAccessor: true` (passwordless
|
|
20
|
+
* Non-spec services marked with `hasClientAccessor: true` (e.g. passwordless)
|
|
21
21
|
* are included in the generated Client struct, constructor, and accessor methods
|
|
22
22
|
* — identical to spec-driven services. Their service type (e.g. PasswordlessService)
|
|
23
23
|
* is defined in a hand-written @oagen-ignore-file, but the Client wiring is generated.
|
package/src/go/enums.ts
CHANGED
|
@@ -96,6 +96,8 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
96
96
|
content: lines.join('\n'),
|
|
97
97
|
overwriteExisting: true,
|
|
98
98
|
});
|
|
99
|
+
const eventConstantsFile = generateEventConstantsFile(enums);
|
|
100
|
+
if (eventConstantsFile) files.push(eventConstantsFile);
|
|
99
101
|
|
|
100
102
|
return files;
|
|
101
103
|
}
|
|
@@ -173,6 +175,81 @@ function collectEnumAliasOf(enums: Enum[]): Map<string, string> {
|
|
|
173
175
|
return aliasOf;
|
|
174
176
|
}
|
|
175
177
|
|
|
178
|
+
function generateEventConstantsFile(enums: Enum[]): GeneratedFile | null {
|
|
179
|
+
const enumDef = findWebhookEventEnum(enums);
|
|
180
|
+
if (!enumDef) return null;
|
|
181
|
+
|
|
182
|
+
const lines: string[] = [];
|
|
183
|
+
lines.push('package events');
|
|
184
|
+
lines.push('');
|
|
185
|
+
lines.push('// Event is a WorkOS event type.');
|
|
186
|
+
lines.push('type Event string');
|
|
187
|
+
lines.push('');
|
|
188
|
+
lines.push('const (');
|
|
189
|
+
|
|
190
|
+
const seenValues = new Set<string>();
|
|
191
|
+
const usedNames = new Set<string>();
|
|
192
|
+
for (const value of enumDef.values) {
|
|
193
|
+
const valueStr = String(value.value);
|
|
194
|
+
if (seenValues.has(valueStr)) continue;
|
|
195
|
+
seenValues.add(valueStr);
|
|
196
|
+
|
|
197
|
+
const constName = uniqueEventConstantName(valueStr, usedNames);
|
|
198
|
+
usedNames.add(constName);
|
|
199
|
+
|
|
200
|
+
if (value.description) {
|
|
201
|
+
lines.push(`\t// ${constName} is ${value.description}.`);
|
|
202
|
+
}
|
|
203
|
+
if (value.deprecated) {
|
|
204
|
+
if (value.description) lines.push('\t//');
|
|
205
|
+
lines.push('\t// Deprecated: this value is deprecated.');
|
|
206
|
+
}
|
|
207
|
+
// Keep constants untyped so callers can use them as plain strings,
|
|
208
|
+
// events.Event values, or typed root-package enum values.
|
|
209
|
+
lines.push(`\t${constName} = "${escapeGoString(valueStr)}"`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
lines.push(')');
|
|
213
|
+
lines.push('');
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
path: 'pkg/events/events.go',
|
|
217
|
+
content: lines.join('\n'),
|
|
218
|
+
overwriteExisting: true,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function findWebhookEventEnum(enums: Enum[]): Enum | null {
|
|
223
|
+
return (
|
|
224
|
+
enums.find((enumDef) => enumDef.name === 'CreateWebhookEndpointEvents') ??
|
|
225
|
+
enums.find(
|
|
226
|
+
(enumDef) =>
|
|
227
|
+
isWebhookEventEnumName(enumDef.name) &&
|
|
228
|
+
enumDef.values.length > 0 &&
|
|
229
|
+
enumDef.values.every((value) => typeof value.value === 'string' && value.value.includes('.')),
|
|
230
|
+
) ??
|
|
231
|
+
null
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function isWebhookEventEnumName(name: string): boolean {
|
|
236
|
+
const normalized = name.toLowerCase();
|
|
237
|
+
return normalized.includes('webhook') && normalized.includes('event');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function uniqueEventConstantName(value: string, usedNames: Set<string>): string {
|
|
241
|
+
const base = className(value);
|
|
242
|
+
if (!usedNames.has(base)) return base;
|
|
243
|
+
|
|
244
|
+
let suffix = 2;
|
|
245
|
+
while (usedNames.has(`${base}${suffix}`)) suffix++;
|
|
246
|
+
return `${base}${suffix}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function escapeGoString(value: string): string {
|
|
250
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
251
|
+
}
|
|
252
|
+
|
|
176
253
|
export function assignEnumsToServices(enums: Enum[], services: Service[]): Map<string, string> {
|
|
177
254
|
const enumToService = new Map<string, string>();
|
|
178
255
|
const enumNames = new Set(enums.map((e) => e.name));
|
package/src/kotlin/enums.ts
CHANGED
|
@@ -41,12 +41,19 @@ export function generateEnums(enums: Enum[], _ctx: EmitterContext): GeneratedFil
|
|
|
41
41
|
|
|
42
42
|
// Within each group, pick the shortest className as canonical.
|
|
43
43
|
const aliasOf = new Map<string, string>(); // enum name → canonical enum name
|
|
44
|
+
const sharedSortEmitters = new Set<string>();
|
|
44
45
|
for (const [, group] of hashGroups) {
|
|
45
|
-
if (group.length <= 1)
|
|
46
|
+
if (group.length <= 1) {
|
|
47
|
+
if (group.length === 1 && isSharedSortOrderEnum(group[0])) {
|
|
48
|
+
enumCanonicalMap.set(group[0].name, 'SortOrder');
|
|
49
|
+
sharedSortEmitters.add(group[0].name);
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
46
53
|
if (group.every(isSharedSortOrderEnum)) {
|
|
47
54
|
const [canonical, ...rest] = [...group].sort((a, b) => a.name.localeCompare(b.name));
|
|
48
|
-
|
|
49
|
-
for (const enumDef of rest) enumCanonicalMap.set(enumDef.name, 'SortOrder');
|
|
55
|
+
sharedSortEmitters.add(canonical.name);
|
|
56
|
+
for (const enumDef of [canonical, ...rest]) enumCanonicalMap.set(enumDef.name, 'SortOrder');
|
|
50
57
|
continue;
|
|
51
58
|
}
|
|
52
59
|
const sorted = [...group].sort(
|
|
@@ -68,7 +75,7 @@ export function generateEnums(enums: Enum[], _ctx: EmitterContext): GeneratedFil
|
|
|
68
75
|
const typeName = canonicalEnumTypeName(enumDef);
|
|
69
76
|
|
|
70
77
|
// Non-canonical enum: emit a typealias instead of a full enum class.
|
|
71
|
-
const sharedSortEmitter =
|
|
78
|
+
const sharedSortEmitter = sharedSortEmitters.has(enumDef.name);
|
|
72
79
|
const canonicalName = sharedSortEmitter
|
|
73
80
|
? undefined
|
|
74
81
|
: (aliasOf.get(enumDef.name) ?? enumCanonicalMap.get(enumDef.name));
|
package/src/node/client.ts
CHANGED
|
@@ -18,9 +18,11 @@ import {
|
|
|
18
18
|
isListWrapperModel,
|
|
19
19
|
computeNonEventReachable,
|
|
20
20
|
} from './utils.js';
|
|
21
|
+
import { isNodeOwnedService, nodeOptions } from './options.js';
|
|
21
22
|
import { resolveResourceClassName, resolveResourceDir } from './resources.js';
|
|
22
23
|
import { generatedResourceInterfaceModelNames } from './models.js';
|
|
23
24
|
import { assignEnumsToServices } from './enums.js';
|
|
25
|
+
import { liveSurfaceInterfacePath } from './live-surface.js';
|
|
24
26
|
|
|
25
27
|
export function generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
26
28
|
const files: GeneratedFile[] = [];
|
|
@@ -129,6 +131,46 @@ function generateWorkOSClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
129
131
|
};
|
|
130
132
|
}
|
|
131
133
|
|
|
134
|
+
function sourceFileForExportedType(ctx: EmitterContext, typeName: string): string | undefined {
|
|
135
|
+
return (
|
|
136
|
+
(ctx.apiSurface?.interfaces?.[typeName] as { sourceFile?: string } | undefined)?.sourceFile ??
|
|
137
|
+
(ctx.apiSurface?.typeAliases?.[typeName] as { sourceFile?: string } | undefined)?.sourceFile ??
|
|
138
|
+
(ctx.apiSurface?.enums?.[typeName] as { sourceFile?: string } | undefined)?.sourceFile ??
|
|
139
|
+
liveSurfaceInterfacePath(typeName)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function exportedNamesForSource(ctx: EmitterContext, sourceFile: string): string[] {
|
|
144
|
+
const names = new Set<string>();
|
|
145
|
+
const addNamesFromSurface = (items: Record<string, unknown> | undefined) => {
|
|
146
|
+
if (!items) return;
|
|
147
|
+
for (const [name, item] of Object.entries(items)) {
|
|
148
|
+
if ((item as { sourceFile?: string }).sourceFile === sourceFile) {
|
|
149
|
+
names.add(name);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
addNamesFromSurface(ctx.apiSurface?.interfaces);
|
|
155
|
+
addNamesFromSurface(ctx.apiSurface?.typeAliases);
|
|
156
|
+
addNamesFromSurface(ctx.apiSurface?.enums);
|
|
157
|
+
|
|
158
|
+
const rootDir = ctx.targetDir ?? ctx.outputDir;
|
|
159
|
+
if (rootDir) {
|
|
160
|
+
try {
|
|
161
|
+
const content = fs.readFileSync(path.join(rootDir, sourceFile), 'utf-8');
|
|
162
|
+
for (const match of content.matchAll(/export\s+(?:interface|type|enum|class|const|function)\s+(\w+)/g)) {
|
|
163
|
+
names.add(match[1]);
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// The live source map is best-effort; apiSurface/livesurface names above
|
|
167
|
+
// are enough when the file is not readable.
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return [...names];
|
|
172
|
+
}
|
|
173
|
+
|
|
132
174
|
/**
|
|
133
175
|
* Generate per-service barrel files (interfaces/index.ts) that re-export
|
|
134
176
|
* all interface and enum files for each service directory. This reduces
|
|
@@ -146,6 +188,12 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
|
|
|
146
188
|
// from one file and a domain type from another).
|
|
147
189
|
const dirExports = new Map<string, string[]>();
|
|
148
190
|
const dirSymbols = new Map<string, Set<string>>();
|
|
191
|
+
const ownedDirNames = new Set<string>();
|
|
192
|
+
for (const service of spec.services) {
|
|
193
|
+
if (isNodeOwnedService(ctx, service.name)) {
|
|
194
|
+
ownedDirNames.add(resolveDir(service.name));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
149
197
|
|
|
150
198
|
// Pre-seed dirSymbols with names already exported by existing interface files.
|
|
151
199
|
// When the existing SDK has an interface file in a directory that already
|
|
@@ -160,6 +208,7 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
|
|
|
160
208
|
const match = sourceFile.match(/^src\/([^/]+)\/interfaces\/(.+)\.ts$/);
|
|
161
209
|
if (!match) return;
|
|
162
210
|
const dirName = match[1];
|
|
211
|
+
if (ownedDirNames.has(dirName)) return;
|
|
163
212
|
const fileStem = match[2];
|
|
164
213
|
if (!dirSymbols.has(dirName)) dirSymbols.set(dirName, new Set());
|
|
165
214
|
dirSymbols.get(dirName)!.add(name);
|
|
@@ -260,14 +309,82 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
|
|
|
260
309
|
dirExports.get(dirName)!.push(`export * from './${fileName(enumDef.name)}.interface';`);
|
|
261
310
|
}
|
|
262
311
|
|
|
312
|
+
const overrideWildcardSources = new Set<string>();
|
|
313
|
+
const overrideNamedExports = new Set<string>();
|
|
314
|
+
const addOverrideTypeExport = (typeName: string | undefined) => {
|
|
315
|
+
if (!typeName) return;
|
|
316
|
+
const sourceFile = sourceFileForExportedType(ctx, typeName);
|
|
317
|
+
if (!sourceFile) return;
|
|
318
|
+
const match = sourceFile?.match(/^src\/([^/]+)\/interfaces\/(.+)\.ts$/);
|
|
319
|
+
if (!match) return;
|
|
320
|
+
const dirName = match[1];
|
|
321
|
+
const stem = match[2].replace(/\.ts$/, '');
|
|
322
|
+
if (!dirExports.has(dirName)) {
|
|
323
|
+
dirExports.set(dirName, []);
|
|
324
|
+
if (!dirSymbols.has(dirName)) {
|
|
325
|
+
dirSymbols.set(dirName, new Set());
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const symbols = dirSymbols.get(dirName)!;
|
|
329
|
+
const sourceKey = `${dirName}/${stem}`;
|
|
330
|
+
if (overrideWildcardSources.has(sourceKey)) return;
|
|
331
|
+
|
|
332
|
+
const sourceNames = exportedNamesForSource(ctx, sourceFile);
|
|
333
|
+
const hasConflictingNeighbor = sourceNames.some((name) => name !== typeName && globalExistingSymbols.has(name));
|
|
334
|
+
if (hasConflictingNeighbor) {
|
|
335
|
+
const exportKey = `${sourceKey}:${typeName}`;
|
|
336
|
+
if (overrideNamedExports.has(exportKey)) return;
|
|
337
|
+
dirExports.get(dirName)!.push(`export type { ${typeName} } from './${stem}';`);
|
|
338
|
+
overrideNamedExports.add(exportKey);
|
|
339
|
+
symbols.add(typeName);
|
|
340
|
+
globalExistingSymbols.add(typeName);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
dirExports.get(dirName)!.push(`export * from './${stem}';`);
|
|
345
|
+
overrideWildcardSources.add(sourceKey);
|
|
346
|
+
const namesToRegister = sourceNames.length > 0 ? sourceNames : [typeName];
|
|
347
|
+
for (const name of namesToRegister) {
|
|
348
|
+
symbols.add(name);
|
|
349
|
+
globalExistingSymbols.add(name);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
for (const override of Object.values(nodeOptions(ctx).operationOverrides ?? {})) {
|
|
354
|
+
addOverrideTypeExport(override.optionsType);
|
|
355
|
+
for (const typeName of override.returnTypeImports ?? []) {
|
|
356
|
+
addOverrideTypeExport(typeName);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const generatedOptionExports = (ctx as any)._nodeGeneratedOptionInterfaceExports as
|
|
361
|
+
| Map<string, Array<{ stem: string; typeName: string }>>
|
|
362
|
+
| undefined;
|
|
363
|
+
for (const [dirName, options] of generatedOptionExports ?? []) {
|
|
364
|
+
if (!dirExports.has(dirName)) {
|
|
365
|
+
dirExports.set(dirName, []);
|
|
366
|
+
if (!dirSymbols.has(dirName)) {
|
|
367
|
+
dirSymbols.set(dirName, new Set());
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const symbols = dirSymbols.get(dirName)!;
|
|
371
|
+
for (const { stem, typeName } of options) {
|
|
372
|
+
if (globalExistingSymbols.has(typeName)) continue;
|
|
373
|
+
symbols.add(typeName);
|
|
374
|
+
globalExistingSymbols.add(typeName);
|
|
375
|
+
dirExports.get(dirName)!.push(`export * from './${stem}';`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
263
379
|
for (const [dirName, exports] of dirExports) {
|
|
264
380
|
const exportSet = new Set(exports);
|
|
381
|
+
const isDirOwned = ownedDirNames.has(dirName);
|
|
265
382
|
|
|
266
383
|
// When integrating into an existing SDK, include baseline exports from
|
|
267
384
|
// the api-surface so the barrel is comprehensive. This ensures stale
|
|
268
385
|
// entries (e.g., renamed files from previous generations) are removed
|
|
269
386
|
// when overwriteExisting replaces the barrel.
|
|
270
|
-
if (ctx.apiSurface) {
|
|
387
|
+
if (ctx.apiSurface && !isDirOwned) {
|
|
271
388
|
const addBaselineExports = (items: Record<string, any> | undefined) => {
|
|
272
389
|
if (!items) return;
|
|
273
390
|
for (const item of Object.values(items)) {
|
|
@@ -288,7 +405,7 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
|
|
|
288
405
|
// corresponding file still exists on disk. This prevents dropping
|
|
289
406
|
// hand-written types (e.g., Factor in multi-factor-auth) when a
|
|
290
407
|
// generated model in the same file causes a symbol collision.
|
|
291
|
-
if (ctx.targetDir) {
|
|
408
|
+
if (ctx.targetDir && !isDirOwned) {
|
|
292
409
|
const interfacesDir = path.join(ctx.targetDir, 'src', dirName, 'interfaces');
|
|
293
410
|
try {
|
|
294
411
|
const barrelPath = path.join(interfacesDir, 'index.ts');
|
|
@@ -69,6 +69,8 @@ interface DiscriminatedShape {
|
|
|
69
69
|
discriminatorProperty: string;
|
|
70
70
|
/** Field name in domain (camelCase). */
|
|
71
71
|
discriminatorPropertyDomain: string;
|
|
72
|
+
/** Description from the OpenAPI spec, if present. */
|
|
73
|
+
discriminatorDescription?: string;
|
|
72
74
|
variants: VariantSpec[];
|
|
73
75
|
}
|
|
74
76
|
|
|
@@ -124,20 +126,23 @@ export function detectDiscriminatedShape(
|
|
|
124
126
|
return {
|
|
125
127
|
nameSuffix: variantNameSuffix(discValue),
|
|
126
128
|
discriminatorValue: discValue,
|
|
127
|
-
fields: variantFields(fv, discProp, modelName
|
|
129
|
+
fields: variantFields(fv, discProp, modelName),
|
|
128
130
|
};
|
|
129
131
|
})
|
|
130
132
|
.filter((v): v is VariantSpec => v !== null);
|
|
131
133
|
|
|
132
134
|
if (variants.length !== flattenedVariants.length) return null;
|
|
133
135
|
|
|
134
|
-
const baseFields = baseObject ? collectObjectFields(baseObject, modelName
|
|
136
|
+
const baseFields = baseObject ? collectObjectFields(baseObject, modelName) : [];
|
|
137
|
+
|
|
138
|
+
const discriminatorDescription = flattenedVariants[0].alwaysProperties.get(discProp)?.description;
|
|
135
139
|
|
|
136
140
|
return {
|
|
137
141
|
modelName,
|
|
138
142
|
baseFields,
|
|
139
143
|
discriminatorProperty: discProp,
|
|
140
144
|
discriminatorPropertyDomain: toCamelCase(discProp),
|
|
145
|
+
discriminatorDescription,
|
|
141
146
|
variants,
|
|
142
147
|
};
|
|
143
148
|
}
|
|
@@ -146,7 +151,7 @@ function mergeBase(prev: RawSchema | null, next: RawSchema): RawSchema {
|
|
|
146
151
|
if (!prev) return next;
|
|
147
152
|
return {
|
|
148
153
|
type: 'object',
|
|
149
|
-
properties: { ...
|
|
154
|
+
properties: { ...prev.properties, ...next.properties },
|
|
150
155
|
required: [...new Set([...(prev.required ?? []), ...(next.required ?? [])])],
|
|
151
156
|
};
|
|
152
157
|
}
|
|
@@ -328,48 +333,33 @@ function variantNameSuffix(constValue: string): string {
|
|
|
328
333
|
// Field extraction
|
|
329
334
|
// ---------------------------------------------------------------------------
|
|
330
335
|
|
|
331
|
-
function collectObjectFields(
|
|
332
|
-
schema: RawSchema,
|
|
333
|
-
parentName: string,
|
|
334
|
-
rawSchemas: Record<string, RawSchema>,
|
|
335
|
-
): FieldSpec[] {
|
|
336
|
+
function collectObjectFields(schema: RawSchema, parentName: string): FieldSpec[] {
|
|
336
337
|
const props = schema.properties ?? {};
|
|
337
338
|
const required = new Set(schema.required ?? []);
|
|
338
339
|
const fields: FieldSpec[] = [];
|
|
339
340
|
for (const [name, propSchema] of Object.entries(props)) {
|
|
340
|
-
fields.push(buildField(name, propSchema, required.has(name), parentName
|
|
341
|
+
fields.push(buildField(name, propSchema, required.has(name), parentName));
|
|
341
342
|
}
|
|
342
343
|
return fields;
|
|
343
344
|
}
|
|
344
345
|
|
|
345
|
-
function variantFields(
|
|
346
|
-
fv: FlattenedVariant,
|
|
347
|
-
discriminatorProperty: string,
|
|
348
|
-
parentName: string,
|
|
349
|
-
rawSchemas: Record<string, RawSchema>,
|
|
350
|
-
): FieldSpec[] {
|
|
346
|
+
function variantFields(fv: FlattenedVariant, discriminatorProperty: string, parentName: string): FieldSpec[] {
|
|
351
347
|
const fields: FieldSpec[] = [];
|
|
352
348
|
for (const [name, propSchema] of fv.alwaysProperties) {
|
|
353
349
|
if (name === discriminatorProperty) continue;
|
|
354
|
-
fields.push(buildField(name, propSchema, fv.required.has(name), parentName
|
|
350
|
+
fields.push(buildField(name, propSchema, fv.required.has(name), parentName));
|
|
355
351
|
}
|
|
356
352
|
for (const [name, propSchema] of fv.optionalProperties) {
|
|
357
353
|
if (name === discriminatorProperty) continue;
|
|
358
|
-
fields.push(buildField(name, propSchema, false, parentName
|
|
354
|
+
fields.push(buildField(name, propSchema, false, parentName));
|
|
359
355
|
}
|
|
360
356
|
return fields;
|
|
361
357
|
}
|
|
362
358
|
|
|
363
|
-
function buildField(
|
|
364
|
-
rawName: string,
|
|
365
|
-
schema: RawSchema,
|
|
366
|
-
required: boolean,
|
|
367
|
-
parentName: string,
|
|
368
|
-
rawSchemas: Record<string, RawSchema>,
|
|
369
|
-
): FieldSpec {
|
|
359
|
+
function buildField(rawName: string, schema: RawSchema, required: boolean, parentName: string): FieldSpec {
|
|
370
360
|
const modelDeps = new Set<string>();
|
|
371
|
-
const domainType = rawSchemaToTS(schema, parentName, rawName, false, modelDeps
|
|
372
|
-
const wireType = rawSchemaToTS(schema, parentName, rawName, true, modelDeps
|
|
361
|
+
const domainType = rawSchemaToTS(schema, parentName, rawName, false, modelDeps);
|
|
362
|
+
const wireType = rawSchemaToTS(schema, parentName, rawName, true, modelDeps);
|
|
373
363
|
return {
|
|
374
364
|
name: rawName,
|
|
375
365
|
description: schema.description,
|
|
@@ -404,7 +394,6 @@ function rawSchemaToTS(
|
|
|
404
394
|
fieldName: string,
|
|
405
395
|
isWire: boolean,
|
|
406
396
|
modelDeps: Set<string>,
|
|
407
|
-
rawSchemas: Record<string, RawSchema>,
|
|
408
397
|
): string {
|
|
409
398
|
if (schema.$ref) {
|
|
410
399
|
const refName = schema.$ref.split('/').pop()!;
|
|
@@ -428,7 +417,7 @@ function rawSchemaToTS(
|
|
|
428
417
|
} else if (baseType === 'boolean') {
|
|
429
418
|
core = 'boolean';
|
|
430
419
|
} else if (baseType === 'array' && schema.items) {
|
|
431
|
-
const items = rawSchemaToTS(schema.items, parentName, singularize(fieldName), isWire, modelDeps
|
|
420
|
+
const items = rawSchemaToTS(schema.items, parentName, singularize(fieldName), isWire, modelDeps);
|
|
432
421
|
core = `${parenthesizeUnion(items)}[]`;
|
|
433
422
|
} else if (baseType === 'object' && schema.properties) {
|
|
434
423
|
// Inline object — refer to the synthetic model name that
|
|
@@ -543,6 +532,9 @@ function buildInterfaceBody(name: string, shape: DiscriminatedShape, variant: Va
|
|
|
543
532
|
}
|
|
544
533
|
// Discriminator (typed as the variant's const value)
|
|
545
534
|
const discKey = isWire ? shape.discriminatorProperty : shape.discriminatorPropertyDomain;
|
|
535
|
+
if (shape.discriminatorDescription) {
|
|
536
|
+
lines.push(` /** ${shape.discriminatorDescription} */`);
|
|
537
|
+
}
|
|
546
538
|
lines.push(` ${discKey}: '${variant.discriminatorValue}';`);
|
|
547
539
|
// Variant-specific fields
|
|
548
540
|
for (const field of variant.fields) {
|