@workos/oagen-emitters 0.14.4 → 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 +12 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{plugin-BGVaMGqe.mjs → plugin-CO4RFgAW.mjs} +959 -251
- package/dist/plugin-CO4RFgAW.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 +119 -2
- package/src/node/discriminated-models.ts +8 -0
- 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/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-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
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,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
|
|
|
@@ -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
|
}
|
|
@@ -527,6 +532,9 @@ function buildInterfaceBody(name: string, shape: DiscriminatedShape, variant: Va
|
|
|
527
532
|
}
|
|
528
533
|
// Discriminator (typed as the variant's const value)
|
|
529
534
|
const discKey = isWire ? shape.discriminatorProperty : shape.discriminatorPropertyDomain;
|
|
535
|
+
if (shape.discriminatorDescription) {
|
|
536
|
+
lines.push(` /** ${shape.discriminatorDescription} */`);
|
|
537
|
+
}
|
|
530
538
|
lines.push(` ${discKey}: '${variant.discriminatorValue}';`);
|
|
531
539
|
// Variant-specific fields
|
|
532
540
|
for (const field of variant.fields) {
|
package/src/node/field-plan.ts
CHANGED
|
@@ -7,8 +7,15 @@ import {
|
|
|
7
7
|
isBaselineGeneric,
|
|
8
8
|
createServiceDirResolver,
|
|
9
9
|
modelHasNewFields,
|
|
10
|
+
assignModelsToServices,
|
|
10
11
|
} from './utils.js';
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
liveSurfaceHasFunction,
|
|
14
|
+
liveSurfaceHasFile,
|
|
15
|
+
liveSurfaceFunctionPath,
|
|
16
|
+
liveSurfaceHasAutogenFile,
|
|
17
|
+
} from './live-surface.js';
|
|
18
|
+
import { isNodeOwnedService } from './options.js';
|
|
12
19
|
|
|
13
20
|
// ---------------------------------------------------------------------------
|
|
14
21
|
// Guard strategy
|
|
@@ -51,6 +58,19 @@ function helperExists(helperName: string, depModelName: string, ctx: EmitterCont
|
|
|
51
58
|
if (liveSurfaceHasFunction(helperName)) return true;
|
|
52
59
|
const depModel = ctx.spec.models.find((m) => m.name === depModelName);
|
|
53
60
|
if (!depModel) return false;
|
|
61
|
+
const modelToService = assignModelsToServices(ctx.spec.models, ctx.spec.services, ctx.modelHints);
|
|
62
|
+
const depService = modelToService.get(depModelName);
|
|
63
|
+
const resolvedName = resolveInterfaceName(depModelName, ctx);
|
|
64
|
+
const siblingPrefix = helperName.startsWith('serialize') ? 'deserialize' : 'serialize';
|
|
65
|
+
const siblingPath = liveSurfaceFunctionPath(`${siblingPrefix}${resolvedName}`);
|
|
66
|
+
if (siblingPath && liveSurfaceHasFile(siblingPath) && !liveSurfaceHasAutogenFile(siblingPath)) return false;
|
|
67
|
+
const sourceFile = (ctx.apiSurface?.interfaces?.[resolvedName] as { sourceFile?: string } | undefined)?.sourceFile;
|
|
68
|
+
const { resolveDir } = createServiceDirResolver(ctx.spec.models, ctx.spec.services, ctx);
|
|
69
|
+
const candidate = sourceFile
|
|
70
|
+
? sourceFile.replace('/interfaces/', '/serializers/').replace('.interface.ts', '.serializer.ts')
|
|
71
|
+
: `src/${resolveDir(depService)}/serializers/${fileName(depModelName)}.serializer.ts`;
|
|
72
|
+
if (liveSurfaceHasFile(candidate) && !liveSurfaceHasAutogenFile(candidate)) return false;
|
|
73
|
+
if (isNodeOwnedService(ctx, depService)) return true;
|
|
54
74
|
return modelHasNewFields(depModel, ctx);
|
|
55
75
|
}
|
|
56
76
|
|
|
@@ -699,6 +719,7 @@ export function buildSerializerImports(
|
|
|
699
719
|
const depService = sctx.modelToService.get(dep);
|
|
700
720
|
const depDir = sctx.resolveDir(depService);
|
|
701
721
|
const depName = resolveInterfaceName(dep, sctx.ctx);
|
|
722
|
+
const depIsOwned = isNodeOwnedService(sctx.ctx, depService);
|
|
702
723
|
|
|
703
724
|
// Locate the serializer file, in priority order:
|
|
704
725
|
// 1. The actual file containing `deserialize${depName}` per
|
|
@@ -708,14 +729,16 @@ export function buildSerializerImports(
|
|
|
708
729
|
// 2. The baseline interface's adjacent serializer file path.
|
|
709
730
|
// 3. The IR-name path — this is where the emitter writes the
|
|
710
731
|
// serializer it's producing this run.
|
|
711
|
-
const baselineSrc =
|
|
732
|
+
const baselineSrc = depIsOwned
|
|
733
|
+
? undefined
|
|
734
|
+
: (sctx.ctx.apiSurface?.interfaces?.[depName] as { sourceFile?: string } | undefined)?.sourceFile;
|
|
712
735
|
const baselineSerializerPath = baselineSrc
|
|
713
736
|
? baselineSrc.replace('/interfaces/', '/serializers/').replace('.interface.ts', '.serializer.ts')
|
|
714
737
|
: null;
|
|
715
738
|
const irNameSerializerPath = `src/${depDir}/serializers/${fileName(dep)}.serializer.ts`;
|
|
716
739
|
|
|
717
|
-
const liveDeserPath = liveSurfaceFunctionPath(`deserialize${depName}`);
|
|
718
|
-
const liveSerPath = liveSurfaceFunctionPath(`serialize${depName}`);
|
|
740
|
+
const liveDeserPath = depIsOwned ? undefined : liveSurfaceFunctionPath(`deserialize${depName}`);
|
|
741
|
+
const liveSerPath = depIsOwned ? undefined : liveSurfaceFunctionPath(`serialize${depName}`);
|
|
719
742
|
const depSerializerPath =
|
|
720
743
|
liveDeserPath ??
|
|
721
744
|
liveSerPath ??
|
|
@@ -742,11 +765,11 @@ export function buildSerializerImports(
|
|
|
742
765
|
// pass-through expression when it can't call the helper.
|
|
743
766
|
const hasDeser = liveSurfaceHasFunction(`deserialize${depName}`);
|
|
744
767
|
const hasSer = liveSurfaceHasFunction(`serialize${depName}`);
|
|
745
|
-
const fileExists = liveSurfaceHasFile(depSerializerPath);
|
|
768
|
+
const fileExists = !depIsOwned && liveSurfaceHasFile(depSerializerPath);
|
|
746
769
|
if (fileExists && !hasDeser && !hasSer) continue;
|
|
747
770
|
if (!fileExists) {
|
|
748
771
|
const depModel = sctx.ctx.spec.models.find((m) => m.name === dep);
|
|
749
|
-
const willGenerateSerializer = depModel ? modelHasNewFields(depModel, sctx.ctx) : true;
|
|
772
|
+
const willGenerateSerializer = depModel ? depIsOwned || modelHasNewFields(depModel, sctx.ctx) : true;
|
|
750
773
|
if (!willGenerateSerializer) continue;
|
|
751
774
|
}
|
|
752
775
|
|
|
@@ -803,7 +826,9 @@ export function shouldSkipSerializeForModel(
|
|
|
803
826
|
skippedSerializeModels: Set<string>,
|
|
804
827
|
ctx: EmitterContext,
|
|
805
828
|
): boolean {
|
|
806
|
-
let shouldSkip =
|
|
829
|
+
let shouldSkip =
|
|
830
|
+
serializerHasBaselineIncompatibility(model, baselineResponse, baselineDomain, ctx) ||
|
|
831
|
+
hasUnsafeSerializePassthrough(model, baselineDomain, baselineResponse, ctx);
|
|
807
832
|
if (!shouldSkip) {
|
|
808
833
|
for (const field of model.fields) {
|
|
809
834
|
for (const ref of collectSerializedModelRefs(field.type)) {
|
|
@@ -816,6 +841,11 @@ export function shouldSkipSerializeForModel(
|
|
|
816
841
|
shouldSkip = true;
|
|
817
842
|
break;
|
|
818
843
|
}
|
|
844
|
+
const resolved = resolveInterfaceName(ref, ctx);
|
|
845
|
+
if (wireInterfaceName(resolved) !== resolved && !helperExists(`serialize${resolved}`, ref, ctx)) {
|
|
846
|
+
shouldSkip = true;
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
819
849
|
}
|
|
820
850
|
if (shouldSkip) break;
|
|
821
851
|
}
|
|
@@ -836,6 +866,8 @@ export function emitSerializerBody(
|
|
|
836
866
|
ctx: EmitterContext,
|
|
837
867
|
): string[] {
|
|
838
868
|
const lines: string[] = [];
|
|
869
|
+
const effectiveShouldSkipSerialize =
|
|
870
|
+
shouldSkipSerialize || hasUnsafeSerializePassthrough(model, baselineDomain, baselineResponse, ctx);
|
|
839
871
|
|
|
840
872
|
if (!shouldSkipDeserialize) {
|
|
841
873
|
const seenDeserFields = new Set<string>();
|
|
@@ -853,7 +885,7 @@ export function emitSerializerBody(
|
|
|
853
885
|
lines.push('});');
|
|
854
886
|
}
|
|
855
887
|
|
|
856
|
-
if (!
|
|
888
|
+
if (!effectiveShouldSkipSerialize) {
|
|
857
889
|
if (!shouldSkipDeserialize) lines.push('');
|
|
858
890
|
const serParamPrefix = model.fields.length === 0 ? '_' : '';
|
|
859
891
|
lines.push(`export const serialize${domainName} = ${typeParams.decl}(`);
|
|
@@ -872,3 +904,27 @@ export function emitSerializerBody(
|
|
|
872
904
|
|
|
873
905
|
return lines;
|
|
874
906
|
}
|
|
907
|
+
|
|
908
|
+
function hasUnsafeSerializePassthrough(
|
|
909
|
+
model: Model,
|
|
910
|
+
baselineDomain: BaselineInterface | undefined,
|
|
911
|
+
baselineResponse: BaselineInterface | undefined,
|
|
912
|
+
ctx: EmitterContext,
|
|
913
|
+
): boolean {
|
|
914
|
+
if (!baselineDomain?.fields || !baselineResponse?.fields) return false;
|
|
915
|
+
|
|
916
|
+
for (const field of model.fields) {
|
|
917
|
+
const domain = fieldName(field.name);
|
|
918
|
+
const wire = wireFieldName(field.name);
|
|
919
|
+
const domainField = baselineDomain.fields[domain];
|
|
920
|
+
const wireField = baselineResponse.fields[wire];
|
|
921
|
+
if (!domainField || !wireField || domainField.type === wireField.type) continue;
|
|
922
|
+
|
|
923
|
+
const domainAccess = `model.${domain}`;
|
|
924
|
+
if (serializeExpression(field.type, domainAccess, ctx) === domainAccess) {
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
return false;
|
|
930
|
+
}
|