@workos/oagen-emitters 0.14.4 → 0.15.1
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-BGVaMGqe.mjs → plugin-C2Hp2Vs2.mjs} +1039 -274
- package/dist/plugin-C2Hp2Vs2.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +7 -7
- 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 +158 -2
- package/src/node/discriminated-models.ts +68 -24
- 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 +553 -89
- 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/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-BGVaMGqe.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-C2Hp2Vs2.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.1",
|
|
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
45
|
"oxfmt": "^0.52.0",
|
|
46
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.21.
|
|
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,19 @@ 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
|
+
const dir = resolveDir(service.name);
|
|
195
|
+
ownedDirNames.add(dir);
|
|
196
|
+
// Ensure owned directories always get a barrel entry, even if no
|
|
197
|
+
// model interfaces are generated (hand-written files still need it).
|
|
198
|
+
if (!dirExports.has(dir)) {
|
|
199
|
+
dirExports.set(dir, []);
|
|
200
|
+
if (!dirSymbols.has(dir)) dirSymbols.set(dir, new Set());
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
149
204
|
|
|
150
205
|
// Pre-seed dirSymbols with names already exported by existing interface files.
|
|
151
206
|
// When the existing SDK has an interface file in a directory that already
|
|
@@ -160,6 +215,7 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
|
|
|
160
215
|
const match = sourceFile.match(/^src\/([^/]+)\/interfaces\/(.+)\.ts$/);
|
|
161
216
|
if (!match) return;
|
|
162
217
|
const dirName = match[1];
|
|
218
|
+
if (ownedDirNames.has(dirName)) return;
|
|
163
219
|
const fileStem = match[2];
|
|
164
220
|
if (!dirSymbols.has(dirName)) dirSymbols.set(dirName, new Set());
|
|
165
221
|
dirSymbols.get(dirName)!.add(name);
|
|
@@ -260,14 +316,82 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
|
|
|
260
316
|
dirExports.get(dirName)!.push(`export * from './${fileName(enumDef.name)}.interface';`);
|
|
261
317
|
}
|
|
262
318
|
|
|
319
|
+
const overrideWildcardSources = new Set<string>();
|
|
320
|
+
const overrideNamedExports = new Set<string>();
|
|
321
|
+
const addOverrideTypeExport = (typeName: string | undefined) => {
|
|
322
|
+
if (!typeName) return;
|
|
323
|
+
const sourceFile = sourceFileForExportedType(ctx, typeName);
|
|
324
|
+
if (!sourceFile) return;
|
|
325
|
+
const match = sourceFile?.match(/^src\/([^/]+)\/interfaces\/(.+)\.ts$/);
|
|
326
|
+
if (!match) return;
|
|
327
|
+
const dirName = match[1];
|
|
328
|
+
const stem = match[2].replace(/\.ts$/, '');
|
|
329
|
+
if (!dirExports.has(dirName)) {
|
|
330
|
+
dirExports.set(dirName, []);
|
|
331
|
+
if (!dirSymbols.has(dirName)) {
|
|
332
|
+
dirSymbols.set(dirName, new Set());
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const symbols = dirSymbols.get(dirName)!;
|
|
336
|
+
const sourceKey = `${dirName}/${stem}`;
|
|
337
|
+
if (overrideWildcardSources.has(sourceKey)) return;
|
|
338
|
+
|
|
339
|
+
const sourceNames = exportedNamesForSource(ctx, sourceFile);
|
|
340
|
+
const hasConflictingNeighbor = sourceNames.some((name) => name !== typeName && globalExistingSymbols.has(name));
|
|
341
|
+
if (hasConflictingNeighbor) {
|
|
342
|
+
const exportKey = `${sourceKey}:${typeName}`;
|
|
343
|
+
if (overrideNamedExports.has(exportKey)) return;
|
|
344
|
+
dirExports.get(dirName)!.push(`export type { ${typeName} } from './${stem}';`);
|
|
345
|
+
overrideNamedExports.add(exportKey);
|
|
346
|
+
symbols.add(typeName);
|
|
347
|
+
globalExistingSymbols.add(typeName);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
dirExports.get(dirName)!.push(`export * from './${stem}';`);
|
|
352
|
+
overrideWildcardSources.add(sourceKey);
|
|
353
|
+
const namesToRegister = sourceNames.length > 0 ? sourceNames : [typeName];
|
|
354
|
+
for (const name of namesToRegister) {
|
|
355
|
+
symbols.add(name);
|
|
356
|
+
globalExistingSymbols.add(name);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
for (const override of Object.values(nodeOptions(ctx).operationOverrides ?? {})) {
|
|
361
|
+
addOverrideTypeExport(override.optionsType);
|
|
362
|
+
for (const typeName of override.returnTypeImports ?? []) {
|
|
363
|
+
addOverrideTypeExport(typeName);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const generatedOptionExports = (ctx as any)._nodeGeneratedOptionInterfaceExports as
|
|
368
|
+
| Map<string, Array<{ stem: string; typeName: string }>>
|
|
369
|
+
| undefined;
|
|
370
|
+
for (const [dirName, options] of generatedOptionExports ?? []) {
|
|
371
|
+
if (!dirExports.has(dirName)) {
|
|
372
|
+
dirExports.set(dirName, []);
|
|
373
|
+
if (!dirSymbols.has(dirName)) {
|
|
374
|
+
dirSymbols.set(dirName, new Set());
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const symbols = dirSymbols.get(dirName)!;
|
|
378
|
+
for (const { stem, typeName } of options) {
|
|
379
|
+
if (globalExistingSymbols.has(typeName)) continue;
|
|
380
|
+
symbols.add(typeName);
|
|
381
|
+
globalExistingSymbols.add(typeName);
|
|
382
|
+
dirExports.get(dirName)!.push(`export * from './${stem}';`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
263
386
|
for (const [dirName, exports] of dirExports) {
|
|
264
387
|
const exportSet = new Set(exports);
|
|
388
|
+
const isDirOwned = ownedDirNames.has(dirName);
|
|
265
389
|
|
|
266
390
|
// When integrating into an existing SDK, include baseline exports from
|
|
267
391
|
// the api-surface so the barrel is comprehensive. This ensures stale
|
|
268
392
|
// entries (e.g., renamed files from previous generations) are removed
|
|
269
393
|
// when overwriteExisting replaces the barrel.
|
|
270
|
-
if (ctx.apiSurface) {
|
|
394
|
+
if (ctx.apiSurface && !isDirOwned) {
|
|
271
395
|
const addBaselineExports = (items: Record<string, any> | undefined) => {
|
|
272
396
|
if (!items) return;
|
|
273
397
|
for (const item of Object.values(items)) {
|
|
@@ -288,7 +412,7 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
|
|
|
288
412
|
// corresponding file still exists on disk. This prevents dropping
|
|
289
413
|
// hand-written types (e.g., Factor in multi-factor-auth) when a
|
|
290
414
|
// generated model in the same file causes a symbol collision.
|
|
291
|
-
if (ctx.targetDir) {
|
|
415
|
+
if (ctx.targetDir && !isDirOwned) {
|
|
292
416
|
const interfacesDir = path.join(ctx.targetDir, 'src', dirName, 'interfaces');
|
|
293
417
|
try {
|
|
294
418
|
const barrelPath = path.join(interfacesDir, 'index.ts');
|
|
@@ -349,6 +473,38 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
|
|
|
349
473
|
}
|
|
350
474
|
}
|
|
351
475
|
|
|
476
|
+
// For owned directories, scan the interfaces directory for hand-written
|
|
477
|
+
// files that still need to be in the barrel (e.g., options interfaces
|
|
478
|
+
// preserved from the baseline).
|
|
479
|
+
const ownedScanRoot = ctx.targetDir ?? ctx.outputDir;
|
|
480
|
+
if (ownedScanRoot && isDirOwned) {
|
|
481
|
+
const interfacesDir = path.join(ownedScanRoot, 'src', dirName, 'interfaces');
|
|
482
|
+
const symbols = dirSymbols.get(dirName) ?? new Set<string>();
|
|
483
|
+
try {
|
|
484
|
+
for (const entry of fs.readdirSync(interfacesDir)) {
|
|
485
|
+
if (entry === 'index.ts') continue;
|
|
486
|
+
if (!entry.endsWith('.ts')) continue;
|
|
487
|
+
const stem = entry.replace(/\.ts$/, '');
|
|
488
|
+
const exportLine = `export * from './${stem}';`;
|
|
489
|
+
if (exportSet.has(exportLine)) continue;
|
|
490
|
+
const content = fs.readFileSync(path.join(interfacesDir, entry), 'utf-8');
|
|
491
|
+
const exportedNames: string[] = [];
|
|
492
|
+
for (const m of content.matchAll(/export\s+(?:interface|type|enum|class|const|function)\s+(\w+)/g)) {
|
|
493
|
+
exportedNames.push(m[1]);
|
|
494
|
+
}
|
|
495
|
+
const hasCollision = exportedNames.some((name) => globalExistingSymbols.has(name));
|
|
496
|
+
if (hasCollision) continue;
|
|
497
|
+
for (const name of exportedNames) {
|
|
498
|
+
symbols.add(name);
|
|
499
|
+
globalExistingSymbols.add(name);
|
|
500
|
+
}
|
|
501
|
+
exportSet.add(exportLine);
|
|
502
|
+
}
|
|
503
|
+
} catch {
|
|
504
|
+
// Directory doesn't exist in target
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
352
508
|
// Deduplicate and sort
|
|
353
509
|
const uniqueExports = [...exportSet];
|
|
354
510
|
uniqueExports.sort();
|
|
@@ -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
|
|
|
@@ -133,11 +135,14 @@ export function detectDiscriminatedShape(
|
|
|
133
135
|
|
|
134
136
|
const baseFields = baseObject ? collectObjectFields(baseObject, modelName) : [];
|
|
135
137
|
|
|
138
|
+
const discriminatorDescription = flattenedVariants[0].alwaysProperties.get(discProp)?.description;
|
|
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
|
}
|
|
@@ -451,6 +456,8 @@ function resolveRef(schema: RawSchema, rawSchemas: Record<string, RawSchema>): R
|
|
|
451
456
|
export interface DiscriminatedPlan {
|
|
452
457
|
shape: DiscriminatedShape;
|
|
453
458
|
modelDir: string;
|
|
459
|
+
/** Maps raw spec schema names to their resolved service directories. */
|
|
460
|
+
depDirMap: Map<string, string>;
|
|
454
461
|
}
|
|
455
462
|
|
|
456
463
|
export function planDiscriminatedModels(models: Model[], ctx: EmitterContext): Map<string, DiscriminatedPlan> {
|
|
@@ -459,11 +466,47 @@ export function planDiscriminatedModels(models: Model[], ctx: EmitterContext): M
|
|
|
459
466
|
if (!spec?.components?.schemas) return plans;
|
|
460
467
|
const rawSchemas = spec.components.schemas as Record<string, RawSchema>;
|
|
461
468
|
const { modelToService, resolveDir } = createServiceDirResolver(models, ctx.spec.services, ctx);
|
|
469
|
+
|
|
470
|
+
// Build a lookup from IR model names to their resolved service directories.
|
|
471
|
+
const irModelDir = new Map<string, string>();
|
|
472
|
+
for (const model of models) {
|
|
473
|
+
irModelDir.set(model.name, resolveDir(modelToService.get(model.name)));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Map raw spec schema names to service directories so discriminated model
|
|
477
|
+
// imports can point to the correct cross-service path. Raw names may differ
|
|
478
|
+
// from IR names due to schemaNameTransform (e.g. Dto stripping).
|
|
479
|
+
const depDirMap = new Map<string, string>();
|
|
480
|
+
for (const rawName of Object.keys(rawSchemas)) {
|
|
481
|
+
if (irModelDir.has(rawName)) {
|
|
482
|
+
depDirMap.set(rawName, irModelDir.get(rawName)!);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const stripped = rawName.replace(/Dto/g, '').replace(/DTO/g, '').replace(/Json$/, '');
|
|
486
|
+
if (stripped !== rawName && irModelDir.has(stripped)) {
|
|
487
|
+
depDirMap.set(rawName, irModelDir.get(stripped)!);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
462
491
|
for (const model of models) {
|
|
463
492
|
const shape = detectDiscriminatedShape(model.name, rawSchemas);
|
|
464
493
|
if (!shape) continue;
|
|
494
|
+
// Skip models whose variant field dependencies can't all be resolved to
|
|
495
|
+
// existing interface files. EventSchema, for instance, references models
|
|
496
|
+
// from many services that may not have generated files yet.
|
|
497
|
+
const allDeps = new Set<string>();
|
|
498
|
+
for (const field of shape.baseFields) {
|
|
499
|
+
for (const d of field.modelDeps) allDeps.add(d);
|
|
500
|
+
}
|
|
501
|
+
for (const variant of shape.variants) {
|
|
502
|
+
for (const field of variant.fields) {
|
|
503
|
+
for (const d of field.modelDeps) allDeps.add(d);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const hasUnresolvableDeps = [...allDeps].some((dep) => !depDirMap.has(dep) && !irModelDir.has(dep));
|
|
507
|
+
if (hasUnresolvableDeps) continue;
|
|
465
508
|
const modelDir = resolveDir(modelToService.get(model.name));
|
|
466
|
-
plans.set(model.name, { shape, modelDir });
|
|
509
|
+
plans.set(model.name, { shape, modelDir, depDirMap });
|
|
467
510
|
}
|
|
468
511
|
return plans;
|
|
469
512
|
}
|
|
@@ -521,12 +564,20 @@ function buildInterfaceFile(plan: DiscriminatedPlan, _ctx: EmitterContext): Gene
|
|
|
521
564
|
function buildInterfaceBody(name: string, shape: DiscriminatedShape, variant: VariantSpec, isWire: boolean): string[] {
|
|
522
565
|
const lines: string[] = [];
|
|
523
566
|
lines.push(`export interface ${name} {`);
|
|
524
|
-
//
|
|
567
|
+
// Variant fields override base fields when both define the same property
|
|
568
|
+
// (variants have narrower types, e.g. `event: 'foo'` vs base `event: string`).
|
|
569
|
+
// The discriminator is also emitted separately as a const literal below.
|
|
570
|
+
const variantFieldNames = new Set(variant.fields.map((f) => f.name));
|
|
525
571
|
for (const field of shape.baseFields) {
|
|
572
|
+
if (variantFieldNames.has(field.name)) continue;
|
|
573
|
+
if (field.name === shape.discriminatorProperty) continue;
|
|
526
574
|
pushFieldLine(lines, field, isWire);
|
|
527
575
|
}
|
|
528
576
|
// Discriminator (typed as the variant's const value)
|
|
529
577
|
const discKey = isWire ? shape.discriminatorProperty : shape.discriminatorPropertyDomain;
|
|
578
|
+
if (shape.discriminatorDescription) {
|
|
579
|
+
lines.push(` /** ${shape.discriminatorDescription} */`);
|
|
580
|
+
}
|
|
530
581
|
lines.push(` ${discKey}: '${variant.discriminatorValue}';`);
|
|
531
582
|
// Variant-specific fields
|
|
532
583
|
for (const field of variant.fields) {
|
|
@@ -561,31 +612,24 @@ function collectImports(plan: DiscriminatedPlan): ImportSpec[] {
|
|
|
561
612
|
for (const d of field.modelDeps) deps.add(d);
|
|
562
613
|
}
|
|
563
614
|
}
|
|
564
|
-
|
|
565
|
-
// We assume all deps live in the same service for now (same dir as this
|
|
566
|
-
// model). Cross-service imports would need ctx.spec.services lookups; the
|
|
567
|
-
// current discriminated-shape cases (ConnectApplication) are all
|
|
568
|
-
// intra-service.
|
|
569
|
-
const symbols: string[] = [];
|
|
615
|
+
const result: ImportSpec[] = [];
|
|
570
616
|
for (const dep of [...deps].sort()) {
|
|
571
617
|
const domain = toPascalCase(dep);
|
|
572
|
-
symbols.push(domain);
|
|
573
618
|
const wire = wireInterfaceName(domain);
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
583
|
-
.
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
}, [] as ImportSpec[]);
|
|
619
|
+
const symbols = wire !== domain ? [domain, wire] : [domain];
|
|
620
|
+
const depDir = plan.depDirMap.get(dep);
|
|
621
|
+
const baseName = fileName(toSnakeFromPascal(domain));
|
|
622
|
+
let importPath: string;
|
|
623
|
+
if (!depDir || depDir === plan.modelDir) {
|
|
624
|
+
importPath = `./${baseName}.interface`;
|
|
625
|
+
} else {
|
|
626
|
+
importPath = `../../${depDir}/interfaces/${baseName}.interface`;
|
|
627
|
+
}
|
|
628
|
+
const existing = result.find((a) => a.path === importPath);
|
|
629
|
+
if (existing) existing.symbols.push(...symbols);
|
|
630
|
+
else result.push({ path: importPath, symbols });
|
|
631
|
+
}
|
|
632
|
+
return result;
|
|
589
633
|
}
|
|
590
634
|
|
|
591
635
|
function toSnakeFromPascal(s: string): string {
|