@workos/oagen-emitters 0.18.4 → 0.19.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 +7 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{plugin-Cciic50q.mjs → plugin-BXDPA9pJ.mjs} +140 -75
- package/dist/plugin-BXDPA9pJ.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +4 -4
- package/src/dotnet/enums.ts +11 -5
- package/src/dotnet/models.ts +11 -5
- package/src/dotnet/resources.ts +3 -3
- package/src/dotnet/tests.ts +3 -3
- package/src/go/resources.ts +3 -3
- package/src/go/tests.ts +3 -3
- package/src/kotlin/enums.ts +21 -11
- package/src/kotlin/models.ts +19 -7
- package/src/kotlin/resources.ts +2 -2
- package/src/kotlin/tests.ts +2 -2
- package/src/node/enums.ts +8 -5
- package/src/node/models.ts +29 -21
- package/src/node/resources.ts +12 -1
- package/src/node/tests.ts +7 -2
- package/src/php/enums.ts +18 -5
- package/src/php/index.ts +11 -3
- package/src/php/models.ts +11 -5
- package/src/php/resources.ts +6 -4
- package/src/php/tests.ts +6 -3
- package/src/python/enums.ts +39 -28
- package/src/python/models.ts +27 -18
- package/src/python/resources.ts +3 -3
- package/src/python/tests.ts +3 -3
- package/src/ruby/enums.ts +28 -19
- package/src/ruby/models.ts +23 -12
- package/src/ruby/rbi.ts +17 -6
- package/src/ruby/resources.ts +2 -2
- package/src/ruby/tests.ts +2 -2
- package/src/rust/enums.ts +9 -1
- package/src/rust/models.ts +18 -5
- package/src/rust/resources.ts +8 -1
- package/src/rust/tests.ts +2 -2
- package/src/shared/resolved-ops.ts +47 -0
- package/test/shared/synthetic-enum-seed.test.ts +79 -0
- package/dist/plugin-Cciic50q.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-BXDPA9pJ.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.19.0",
|
|
4
4
|
"description": "WorkOS' oagen emitters",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "WorkOS",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"@commitlint/config-conventional": "^21.0.2",
|
|
43
43
|
"@types/node": "^26.0.0",
|
|
44
44
|
"husky": "^9.1.7",
|
|
45
|
-
"oxfmt": "^0.
|
|
46
|
-
"oxlint": "^1.
|
|
45
|
+
"oxfmt": "^0.56.0",
|
|
46
|
+
"oxlint": "^1.71.0",
|
|
47
47
|
"prettier": "^3.8.4",
|
|
48
48
|
"tsdown": "^0.22.3",
|
|
49
49
|
"tsx": "^4.22.4",
|
|
@@ -54,6 +54,6 @@
|
|
|
54
54
|
"node": ">=24.10.0"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@workos/oagen": "^0.
|
|
57
|
+
"@workos/oagen": "^0.23.0"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/src/dotnet/enums.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { walkTypeRef } from '@workos/oagen';
|
|
|
3
3
|
import { className, deprecationMessage, escapeCsAttributeString, humanize } from './naming.js';
|
|
4
4
|
import { setEnumAliases, setSingleValueEnumNames } from './type-map.js';
|
|
5
5
|
import { enrichModelsFromSpec } from '../shared/model-utils.js';
|
|
6
|
+
import { isEnumInScope } from '../shared/resolved-ops.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Generate C# enum definitions from IR Enum definitions.
|
|
@@ -135,11 +136,16 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
135
136
|
lines.push(' }');
|
|
136
137
|
lines.push('}');
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
// FR-1.4: write the per-enum FILE only when in scope. .NET uses a flat
|
|
140
|
+
// Enums/ directory with C# namespaces (no barrel/index), so an
|
|
141
|
+
// out-of-scope enum left untouched on disk stays referenceable.
|
|
142
|
+
if (isEnumInScope(enumDef.name, ctx)) {
|
|
143
|
+
files.push({
|
|
144
|
+
path: `Enums/${typeName}.cs`,
|
|
145
|
+
content: lines.join('\n'),
|
|
146
|
+
overwriteExisting: true,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
143
149
|
}
|
|
144
150
|
|
|
145
151
|
return files;
|
package/src/dotnet/models.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
isListMetadataModel,
|
|
27
27
|
collectNonPaginatedResponseModelNames,
|
|
28
28
|
} from '../shared/model-utils.js';
|
|
29
|
+
import { isModelInScope } from '../shared/resolved-ops.js';
|
|
29
30
|
export { isListWrapperModel, isListMetadataModel };
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -355,11 +356,16 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
|
|
|
355
356
|
lines.push(' }');
|
|
356
357
|
lines.push('}');
|
|
357
358
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
359
|
+
// FR-1.4: write the per-model FILE only when in scope. .NET uses a flat
|
|
360
|
+
// Entities/ directory with C# namespaces (no barrel/index), so an
|
|
361
|
+
// out-of-scope model left untouched on disk stays referenceable.
|
|
362
|
+
if (isModelInScope(model.name, ctx)) {
|
|
363
|
+
files.push({
|
|
364
|
+
path: `Entities/${csClassName}.cs`,
|
|
365
|
+
content: lines.join('\n'),
|
|
366
|
+
overwriteExisting: true,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
363
369
|
}
|
|
364
370
|
|
|
365
371
|
return files;
|
package/src/dotnet/resources.ts
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
import {
|
|
36
36
|
buildResolvedLookup,
|
|
37
37
|
lookupResolved,
|
|
38
|
-
|
|
38
|
+
scopedMountGroups,
|
|
39
39
|
getOpDefaults,
|
|
40
40
|
getOpInferFromClient,
|
|
41
41
|
buildHiddenParams,
|
|
@@ -71,10 +71,10 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
71
71
|
if (services.length === 0) return [];
|
|
72
72
|
|
|
73
73
|
const files: GeneratedFile[] = [];
|
|
74
|
-
const mountGroups =
|
|
74
|
+
const mountGroups = scopedMountGroups(ctx);
|
|
75
75
|
|
|
76
76
|
const entries: Array<{ name: string; operations: Operation[] }> =
|
|
77
|
-
mountGroups.size > 0
|
|
77
|
+
mountGroups.size > 0 || ctx.scopedServices?.size
|
|
78
78
|
? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
|
|
79
79
|
: services.map((s) => ({ name: resolveResourceClassName(s, ctx), operations: s.operations }));
|
|
80
80
|
|
package/src/dotnet/tests.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { resolveResourceClassName, sortPathParamsByTemplateOrder, optionsClassNa
|
|
|
16
16
|
import { generateFixtures, generateModelFixture } from './fixtures.js';
|
|
17
17
|
import { isListWrapperModel } from './models.js';
|
|
18
18
|
import {
|
|
19
|
-
|
|
19
|
+
scopedMountGroups,
|
|
20
20
|
buildResolvedLookup,
|
|
21
21
|
lookupResolved,
|
|
22
22
|
buildHiddenParams,
|
|
@@ -40,9 +40,9 @@ export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// Generate per-mount-target test files
|
|
43
|
-
const mountGroups =
|
|
43
|
+
const mountGroups = scopedMountGroups(ctx);
|
|
44
44
|
const testEntries: Array<{ name: string; operations: Operation[] }> =
|
|
45
|
-
mountGroups.size > 0
|
|
45
|
+
mountGroups.size > 0 || ctx.scopedServices?.size
|
|
46
46
|
? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
|
|
47
47
|
: spec.services.map((s) => ({
|
|
48
48
|
name: resolveResourceClassName(s, ctx),
|
package/src/go/resources.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
import {
|
|
22
22
|
buildResolvedLookup,
|
|
23
23
|
lookupResolved,
|
|
24
|
-
|
|
24
|
+
scopedMountGroups,
|
|
25
25
|
getOpDefaults,
|
|
26
26
|
getOpInferFromClient,
|
|
27
27
|
buildHiddenParams,
|
|
@@ -60,11 +60,11 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
60
60
|
if (services.length === 0) return [];
|
|
61
61
|
|
|
62
62
|
const files: GeneratedFile[] = [];
|
|
63
|
-
const mountGroups =
|
|
63
|
+
const mountGroups = scopedMountGroups(ctx);
|
|
64
64
|
|
|
65
65
|
// If no resolved operations, fall back to raw services
|
|
66
66
|
const entries: Array<{ name: string; operations: Operation[] }> =
|
|
67
|
-
mountGroups.size > 0
|
|
67
|
+
mountGroups.size > 0 || ctx.scopedServices?.size
|
|
68
68
|
? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
|
|
69
69
|
: services.map((s) => ({ name: resolveResourceClassName(s, ctx), operations: s.operations }));
|
|
70
70
|
|
package/src/go/tests.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { resolveResourceClassName, paramsStructName, sortPathParamsByTemplateOrd
|
|
|
5
5
|
import { buildServiceAccessPaths } from './client.js';
|
|
6
6
|
import { generateFixtures } from './fixtures.js';
|
|
7
7
|
import { isListWrapperModel } from './models.js';
|
|
8
|
-
import {
|
|
8
|
+
import { scopedMountGroups, buildResolvedLookup, lookupResolved, buildHiddenParams } from '../shared/resolved-ops.js';
|
|
9
9
|
import { existsSync, readFileSync } from 'node:fs';
|
|
10
10
|
import { resolve } from 'node:path';
|
|
11
11
|
|
|
@@ -85,9 +85,9 @@ export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
85
85
|
const accessPaths = buildServiceAccessPaths(spec.services, ctx);
|
|
86
86
|
|
|
87
87
|
// Generate per-mount-target test files
|
|
88
|
-
const mountGroups =
|
|
88
|
+
const mountGroups = scopedMountGroups(ctx);
|
|
89
89
|
const testEntries: Array<{ name: string; operations: Operation[] }> =
|
|
90
|
-
mountGroups.size > 0
|
|
90
|
+
mountGroups.size > 0 || ctx.scopedServices?.size
|
|
91
91
|
? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
|
|
92
92
|
: spec.services.map((s) => ({
|
|
93
93
|
name: resolveResourceClassName(s, ctx),
|
package/src/kotlin/enums.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Enum, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
2
2
|
import { className, ktStringLiteral } from './naming.js';
|
|
3
|
+
import { isEnumInScope } from '../shared/resolved-ops.js';
|
|
3
4
|
|
|
4
5
|
const KOTLIN_SRC_PREFIX = 'src/main/kotlin/';
|
|
5
6
|
const ENUMS_PACKAGE = 'com.workos.types';
|
|
@@ -24,7 +25,7 @@ export const enumCanonicalMap = new Map<string, string>();
|
|
|
24
25
|
* shortest PascalCase name becomes canonical and the rest emit `typealias`
|
|
25
26
|
* files pointing at the canonical class.
|
|
26
27
|
*/
|
|
27
|
-
export function generateEnums(enums: Enum[],
|
|
28
|
+
export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
28
29
|
if (enums.length === 0) return [];
|
|
29
30
|
|
|
30
31
|
// Reset the canonical map on every generation run (guards against re-entry).
|
|
@@ -74,6 +75,11 @@ export function generateEnums(enums: Enum[], _ctx: EmitterContext): GeneratedFil
|
|
|
74
75
|
|
|
75
76
|
const typeName = canonicalEnumTypeName(enumDef);
|
|
76
77
|
|
|
78
|
+
// FR-1.4: write per-enum FILES only when in scope. Each enum is its own
|
|
79
|
+
// `.kt` file (no barrel), so an out-of-scope enum left untouched on disk
|
|
80
|
+
// stays importable.
|
|
81
|
+
const enumInScope = isEnumInScope(enumDef.name, ctx);
|
|
82
|
+
|
|
77
83
|
// Non-canonical enum: emit a typealias instead of a full enum class.
|
|
78
84
|
const sharedSortEmitter = sharedSortEmitters.has(enumDef.name);
|
|
79
85
|
const canonicalName = sharedSortEmitter
|
|
@@ -94,11 +100,13 @@ export function generateEnums(enums: Enum[], _ctx: EmitterContext): GeneratedFil
|
|
|
94
100
|
aliasLine,
|
|
95
101
|
'',
|
|
96
102
|
].join('\n');
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
if (enumInScope) {
|
|
104
|
+
files.push({
|
|
105
|
+
path: `${KOTLIN_SRC_PREFIX}${ENUMS_DIR}/${typeName}.kt`,
|
|
106
|
+
content: aliasContent,
|
|
107
|
+
overwriteExisting: true,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
102
110
|
continue;
|
|
103
111
|
}
|
|
104
112
|
|
|
@@ -175,11 +183,13 @@ export function generateEnums(enums: Enum[], _ctx: EmitterContext): GeneratedFil
|
|
|
175
183
|
lines.push('}');
|
|
176
184
|
lines.push('');
|
|
177
185
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
186
|
+
if (enumInScope) {
|
|
187
|
+
files.push({
|
|
188
|
+
path: `${KOTLIN_SRC_PREFIX}${ENUMS_DIR}/${typeName}.kt`,
|
|
189
|
+
content: lines.join('\n'),
|
|
190
|
+
overwriteExisting: true,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
183
193
|
}
|
|
184
194
|
|
|
185
195
|
return files;
|
package/src/kotlin/models.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
collectNonPaginatedResponseModelNames,
|
|
9
9
|
collectReferencedListMetadataModels,
|
|
10
10
|
} from '../shared/model-utils.js';
|
|
11
|
+
import { isModelInScope } from '../shared/resolved-ops.js';
|
|
11
12
|
|
|
12
13
|
const KOTLIN_SRC_PREFIX = 'src/main/kotlin/';
|
|
13
14
|
const MODELS_PACKAGE = 'com.workos.models';
|
|
@@ -123,10 +124,17 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
123
124
|
for (const model of models) {
|
|
124
125
|
if (skipAsListWrapper(model) || skipAsListMetadata(model)) continue;
|
|
125
126
|
const typeName = className(model.name);
|
|
127
|
+
// FR-1.4: write per-model FILES only when in scope. Each model is its own
|
|
128
|
+
// `.kt` file (no barrel), so an out-of-scope model left untouched on disk
|
|
129
|
+
// stays importable. The WorkOSEvent sealed interface below is an aggregate
|
|
130
|
+
// built from many event models, so it is NOT gated.
|
|
131
|
+
const modelInScope = isModelInScope(model.name, ctx);
|
|
126
132
|
|
|
127
133
|
// Parent of a discriminated union: emit a sealed class.
|
|
128
134
|
if (model.fields.length === 0 && discriminatedUnions.has(typeName)) {
|
|
129
|
-
|
|
135
|
+
if (modelInScope) {
|
|
136
|
+
files.push(emitSealedUnion(typeName, discriminatedUnions.get(typeName)!));
|
|
137
|
+
}
|
|
130
138
|
continue;
|
|
131
139
|
}
|
|
132
140
|
|
|
@@ -142,15 +150,19 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
142
150
|
`typealias ${typeName} = ${canonicalType}`,
|
|
143
151
|
'',
|
|
144
152
|
].join('\n');
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
153
|
+
if (modelInScope) {
|
|
154
|
+
files.push({
|
|
155
|
+
path: `${KOTLIN_SRC_PREFIX}${MODELS_DIR}/${typeName}.kt`,
|
|
156
|
+
content: aliasContent,
|
|
157
|
+
overwriteExisting: true,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
150
160
|
continue;
|
|
151
161
|
}
|
|
152
162
|
|
|
153
|
-
|
|
163
|
+
if (modelInScope) {
|
|
164
|
+
files.push(emitDataClass(model));
|
|
165
|
+
}
|
|
154
166
|
}
|
|
155
167
|
|
|
156
168
|
// Generate the sealed WorkOSEvent interface. Collect all event envelope
|
package/src/kotlin/resources.ts
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
import {
|
|
35
35
|
buildResolvedLookup,
|
|
36
36
|
lookupResolved,
|
|
37
|
-
|
|
37
|
+
scopedMountGroups,
|
|
38
38
|
buildHiddenParams,
|
|
39
39
|
getOpDefaults,
|
|
40
40
|
getOpInferFromClient,
|
|
@@ -97,7 +97,7 @@ function promoteFieldType(f: Field): Field {
|
|
|
97
97
|
export function generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[] {
|
|
98
98
|
if (services.length === 0) return [];
|
|
99
99
|
|
|
100
|
-
const mountGroups =
|
|
100
|
+
const mountGroups = scopedMountGroups(ctx);
|
|
101
101
|
if (mountGroups.size === 0) return [];
|
|
102
102
|
|
|
103
103
|
const files: GeneratedFile[] = [];
|
package/src/kotlin/tests.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from './naming.js';
|
|
23
23
|
import { mapTypeRef } from './type-map.js';
|
|
24
24
|
import {
|
|
25
|
-
|
|
25
|
+
scopedMountGroups,
|
|
26
26
|
lookupResolved,
|
|
27
27
|
buildResolvedLookup,
|
|
28
28
|
buildHiddenParams,
|
|
@@ -76,7 +76,7 @@ function promoteIso8601TypeRef(type: TypeRef, description: string | undefined):
|
|
|
76
76
|
*/
|
|
77
77
|
export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
78
78
|
const files: GeneratedFile[] = [];
|
|
79
|
-
const mountGroups =
|
|
79
|
+
const mountGroups = scopedMountGroups(ctx);
|
|
80
80
|
const resolvedLookup = buildResolvedLookup(ctx);
|
|
81
81
|
|
|
82
82
|
const exportedClasses = buildExportedClassNameSet(ctx);
|
package/src/node/enums.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { docComment, assignModelsToEmittableServices } from './utils.js';
|
|
|
5
5
|
import { isInlineEnum } from './type-map.js';
|
|
6
6
|
import { isNodeOwnedService } from './options.js';
|
|
7
7
|
import { liveSurfaceConstEnumMembers, liveSurfaceInterfacePath } from './live-surface.js';
|
|
8
|
+
import { isEnumInScope } from '../shared/resolved-ops.js';
|
|
8
9
|
|
|
9
10
|
export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
10
11
|
if (enums.length === 0) return [];
|
|
@@ -120,11 +121,13 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
120
121
|
lines.push(` (typeof ${enumDef.name})[keyof typeof ${enumDef.name}];`);
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
if (isEnumInScope(enumDef.name, ctx)) {
|
|
125
|
+
files.push({
|
|
126
|
+
path: `src/${dirName}/interfaces/${fileName(enumDef.name)}.interface.ts`,
|
|
127
|
+
content: lines.join('\n'),
|
|
128
|
+
skipIfExists: !hasNewValues,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
return files;
|
package/src/node/models.ts
CHANGED
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
import { liveSurfaceHasExistingSdk, liveSurfaceHasManagedFile, liveSurfaceInterfacePath } from './live-surface.js';
|
|
46
46
|
import { isNodeOwnedService, isHandOwnedType } from './options.js';
|
|
47
47
|
import { unwrapListModel } from './fixtures.js';
|
|
48
|
-
import { groupByMount, buildResolvedLookup, lookupResolved } from '../shared/resolved-ops.js';
|
|
48
|
+
import { groupByMount, buildResolvedLookup, lookupResolved, isModelInScope } from '../shared/resolved-ops.js';
|
|
49
49
|
import { resolveWrapperParams } from '../shared/wrapper-utils.js';
|
|
50
50
|
import { collectWrapperResponseModels } from './wrappers.js';
|
|
51
51
|
import { resolveResourceClassName } from './resources.js';
|
|
@@ -308,11 +308,13 @@ export function generateModels(models: Model[], ctx: EmitterContext, shared?: Sh
|
|
|
308
308
|
const aliasLines = importSymbols
|
|
309
309
|
? [`import type { ${importSymbols} } from '${canonRelPath}';`, '', ...aliasExports]
|
|
310
310
|
: [...aliasExports];
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
311
|
+
if (isModelInScope(model.name, ctx)) {
|
|
312
|
+
files.push({
|
|
313
|
+
path: aliasPath,
|
|
314
|
+
content: aliasLines.join('\n'),
|
|
315
|
+
overwriteExisting: true,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
316
318
|
continue;
|
|
317
319
|
}
|
|
318
320
|
|
|
@@ -745,11 +747,13 @@ export function generateModels(models: Model[], ctx: EmitterContext, shared?: Sh
|
|
|
745
747
|
}
|
|
746
748
|
}
|
|
747
749
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
750
|
+
if (isModelInScope(model.name, ctx)) {
|
|
751
|
+
files.push({
|
|
752
|
+
path: filePath,
|
|
753
|
+
content: pruneUnusedImports(lines).join('\n'),
|
|
754
|
+
overwriteExisting: true,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
753
757
|
}
|
|
754
758
|
|
|
755
759
|
return files;
|
|
@@ -943,11 +947,13 @@ export function generateSerializers(
|
|
|
943
947
|
parts.push(`serialize${canonDomainName} as serialize${domainName}`);
|
|
944
948
|
}
|
|
945
949
|
const reexportContent = `export { ${parts.join(', ')} } from '${rel}';`;
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
950
|
+
if (isModelInScope(model.name, ctx)) {
|
|
951
|
+
files.push({
|
|
952
|
+
path: serializerPath,
|
|
953
|
+
content: reexportContent,
|
|
954
|
+
overwriteExisting: true,
|
|
955
|
+
});
|
|
956
|
+
}
|
|
951
957
|
continue;
|
|
952
958
|
}
|
|
953
959
|
// The alias is response-reachable, but the canonical model is
|
|
@@ -997,11 +1003,13 @@ export function generateSerializers(
|
|
|
997
1003
|
),
|
|
998
1004
|
];
|
|
999
1005
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1006
|
+
if (isModelInScope(model.name, ctx)) {
|
|
1007
|
+
files.push({
|
|
1008
|
+
path: serializerPath,
|
|
1009
|
+
content: pruneUnusedImports(lines).join('\n'),
|
|
1010
|
+
overwriteExisting: true,
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1005
1013
|
}
|
|
1006
1014
|
|
|
1007
1015
|
(ctx as any)._skippedSerializeModels = skippedSerializeModels;
|
package/src/node/resources.ts
CHANGED
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
buildResolvedLookup,
|
|
79
79
|
lookupResolved,
|
|
80
80
|
groupByMount,
|
|
81
|
+
isMountInScope,
|
|
81
82
|
getOpDefaults,
|
|
82
83
|
getOpInferFromClient,
|
|
83
84
|
} from '../shared/resolved-ops.js';
|
|
@@ -718,11 +719,18 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
718
719
|
// multiple IR services mount to the same resource class.
|
|
719
720
|
const mountGroups = groupByMount(ctx);
|
|
720
721
|
const mergedServices: Service[] =
|
|
721
|
-
mountGroups.size > 0
|
|
722
|
+
mountGroups.size > 0 || ctx.scopedServices?.size
|
|
723
|
+
? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
|
|
724
|
+
: services;
|
|
722
725
|
|
|
723
726
|
const topLevelEnumNames = new Set(ctx.spec.enums.map((e) => e.name));
|
|
724
727
|
|
|
725
728
|
for (const service of mergedServices) {
|
|
729
|
+
// Scope gate: in a scoped (`--services`) run, only emit per-service resource
|
|
730
|
+
// files for the selected post-mount names. `service.name` is the mount-group
|
|
731
|
+
// key (the POST-MOUNT name that matches `ctx.scopedServices`). Applied as an
|
|
732
|
+
// additional early continue ahead of the node-owned/coverage skip logic.
|
|
733
|
+
if (!isMountInScope(service.name, ctx)) continue;
|
|
726
734
|
const isOwnedService = isNodeOwnedService(ctx, service.name, resolveResourceClassName(service, ctx));
|
|
727
735
|
if (!isOwnedService && isServiceCoveredByExisting(service, ctx)) {
|
|
728
736
|
if (!hasMethodsAbsentFromBaseline(service, ctx)) {
|
|
@@ -770,6 +778,9 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
770
778
|
// stable. Placing them under `interfaces/` lets the per-service barrel
|
|
771
779
|
// pick them up automatically.
|
|
772
780
|
for (const service of mergedServices) {
|
|
781
|
+
// Scope gate: keep the per-service options interfaces aligned with the
|
|
782
|
+
// resource files emitted above — only the selected post-mount names.
|
|
783
|
+
if (!isMountInScope(service.name, ctx)) continue;
|
|
773
784
|
const isOwnedService = isNodeOwnedService(ctx, service.name, resolveResourceClassName(service, ctx));
|
|
774
785
|
if (!isOwnedService && isServiceCoveredByExisting(service, ctx) && !hasMethodsAbsentFromBaseline(service, ctx))
|
|
775
786
|
continue;
|
package/src/node/tests.ts
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
modelHasNewFields,
|
|
36
36
|
computeNonEventReachable,
|
|
37
37
|
} from './utils.js';
|
|
38
|
-
import { groupByMount, buildResolvedLookup, lookupResolved } from '../shared/resolved-ops.js';
|
|
38
|
+
import { groupByMount, buildResolvedLookup, lookupResolved, isMountInScope } from '../shared/resolved-ops.js';
|
|
39
39
|
import { isNodeOwnedService, nodeOptions, planOperationFor } from './options.js';
|
|
40
40
|
|
|
41
41
|
type BaselineMethod = {
|
|
@@ -137,7 +137,7 @@ export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
const testEntries: Array<{ name: string; operations: Operation[] }> =
|
|
140
|
-
mountGroups.size > 0
|
|
140
|
+
mountGroups.size > 0 || ctx.scopedServices?.size
|
|
141
141
|
? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
|
|
142
142
|
: spec.services.map((s) => ({ name: resolveResourceClassName(s, ctx), operations: s.operations }));
|
|
143
143
|
|
|
@@ -159,6 +159,11 @@ export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
for (const { name: mountName, operations } of testEntries) {
|
|
162
|
+
// Scope gate: in a scoped (`--services`) run, only emit per-service test
|
|
163
|
+
// files for the selected post-mount names. `mountName` is the mount-group
|
|
164
|
+
// key (the POST-MOUNT name that matches `ctx.scopedServices`). Applied as an
|
|
165
|
+
// additional early continue ahead of the node-owned/coverage skip logic.
|
|
166
|
+
if (!isMountInScope(mountName, ctx)) continue;
|
|
162
167
|
if (operations.length === 0) continue;
|
|
163
168
|
const mergedService: Service = { name: mountName, operations };
|
|
164
169
|
const isOwnedService = isNodeOwnedService(ctx, mountName, resolveResourceClassName(mergedService, ctx));
|
package/src/php/enums.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Enum, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
|
2
2
|
import { toPascalCase } from '@workos/oagen';
|
|
3
3
|
import { className, resolveEnumName } from './naming.js';
|
|
4
4
|
import { phpDocComment } from './utils.js';
|
|
5
|
+
import { isEnumInScope } from '../shared/resolved-ops.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Generate PHP enum files from IR enums.
|
|
@@ -17,6 +18,16 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
17
18
|
if (emittedCanonical.has(canonical)) continue; // skip aliases
|
|
18
19
|
emittedCanonical.add(canonical);
|
|
19
20
|
|
|
21
|
+
// FR-1.4: write the per-enum FILE only when in scope. PHP dedupes
|
|
22
|
+
// value-identical enums onto a single canonical class, so the canonical
|
|
23
|
+
// file is needed when EITHER the canonical name OR any alias resolving to
|
|
24
|
+
// it is reachable from the selected services. PSR-4 (one class per file
|
|
25
|
+
// under lib/Resource/, no barrel) means an out-of-scope enum is simply
|
|
26
|
+
// left untouched on disk and stays loadable.
|
|
27
|
+
const enumInScope = enums.some(
|
|
28
|
+
(other) => resolveEnumName(other.name) === canonical && isEnumInScope(other.name, ctx),
|
|
29
|
+
);
|
|
30
|
+
|
|
20
31
|
const name = className(canonical);
|
|
21
32
|
const _isAllStrings = e.values.every((v) => typeof v.value === 'string');
|
|
22
33
|
const isAllInts = e.values.every((v) => typeof v.value === 'number' && Number.isInteger(v.value));
|
|
@@ -56,11 +67,13 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
56
67
|
|
|
57
68
|
lines.push('}');
|
|
58
69
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
if (enumInScope) {
|
|
71
|
+
files.push({
|
|
72
|
+
path: `lib/Resource/${name}.php`,
|
|
73
|
+
content: lines.join('\n'),
|
|
74
|
+
overwriteExisting: true,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
return files;
|
package/src/php/index.ts
CHANGED
|
@@ -42,9 +42,17 @@ function ensureTrailingNewlines(files: GeneratedFile[]): GeneratedFile[] {
|
|
|
42
42
|
* classes (no sum types), so a discriminated base whose IR fields the
|
|
43
43
|
* parser stripped (post-allOf-aware detection) gets its original fields
|
|
44
44
|
* restored to avoid silently dropping variant data.
|
|
45
|
+
*
|
|
46
|
+
* `enums` is forwarded to seed `enrichModelsFromSpec`'s collision set: an
|
|
47
|
+
* inline oneOf enum whose synthetic name (`Parent_field`) snake-collapses
|
|
48
|
+
* onto an existing IR enum (e.g. `DataIntegrationAccessTokenResponse_error`
|
|
49
|
+
* vs `DataIntegrationAccessTokenResponseError`) must NOT spawn a duplicate
|
|
50
|
+
* synthetic. Otherwise both collapse to the same `lib/Resource/X.php` path
|
|
51
|
+
* and the later writer wins by array order — which differs between a full
|
|
52
|
+
* and a scoped (`--services`) run, producing a non-deterministic case order.
|
|
45
53
|
*/
|
|
46
|
-
function enrichModelsForPhp(models: Model[]): Model[] {
|
|
47
|
-
const enriched = enrichModelsFromSpec(models);
|
|
54
|
+
function enrichModelsForPhp(models: Model[], enums: Enum[]): Model[] {
|
|
55
|
+
const enriched = enrichModelsFromSpec(models, enums);
|
|
48
56
|
const originalByName = new Map(models.map((m) => [m.name, m]));
|
|
49
57
|
return enriched.map((m) => {
|
|
50
58
|
if ((m as { discriminator?: unknown }).discriminator && m.fields.length === 0) {
|
|
@@ -62,7 +70,7 @@ export const phpEmitter: Emitter = {
|
|
|
62
70
|
|
|
63
71
|
generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[] {
|
|
64
72
|
ensureNamingInitialized(ctx);
|
|
65
|
-
return ensureTrailingNewlines(generateModels(enrichModelsForPhp(models), ctx));
|
|
73
|
+
return ensureTrailingNewlines(generateModels(enrichModelsForPhp(models, ctx.spec.enums), ctx));
|
|
66
74
|
},
|
|
67
75
|
|
|
68
76
|
generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
package/src/php/models.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
collectNonPaginatedResponseModelNames,
|
|
11
11
|
collectReferencedListMetadataModels,
|
|
12
12
|
} from '../shared/model-utils.js';
|
|
13
|
+
import { isModelInScope } from '../shared/resolved-ops.js';
|
|
13
14
|
export { isListMetadataModel, isListWrapperModel };
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -140,11 +141,16 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
140
141
|
|
|
141
142
|
lines.push('}');
|
|
142
143
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
// FR-1.4: write the per-model FILE only when in scope. PHP uses PSR-4
|
|
145
|
+
// (one class per file under lib/Resource/, no barrel/index), so an
|
|
146
|
+
// out-of-scope model is simply left untouched on disk and stays loadable.
|
|
147
|
+
if (isModelInScope(model.name, ctx)) {
|
|
148
|
+
files.push({
|
|
149
|
+
path: `lib/Resource/${name}.php`,
|
|
150
|
+
content: lines.join('\n'),
|
|
151
|
+
overwriteExisting: true,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
148
154
|
}
|
|
149
155
|
|
|
150
156
|
return files;
|
package/src/php/resources.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { mapTypeRef, mapTypeRefForPHPDoc } from './type-map.js';
|
|
|
12
12
|
import { className, fieldName, resolveMethodName, buildExportedClassNameSet, resolveServiceTarget } from './naming.js';
|
|
13
13
|
import { isListWrapperModel } from './models.js';
|
|
14
14
|
import {
|
|
15
|
-
|
|
15
|
+
scopedMountGroups,
|
|
16
16
|
buildResolvedLookup,
|
|
17
17
|
lookupResolved,
|
|
18
18
|
getOpDefaults,
|
|
@@ -44,10 +44,12 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
44
44
|
const files: GeneratedFile[] = [];
|
|
45
45
|
const modelMap = new Map(ctx.spec.models.map((m) => [m.name, m]));
|
|
46
46
|
|
|
47
|
-
// Group operations by mount target
|
|
48
|
-
|
|
47
|
+
// Group operations by mount target. In a scoped (`--services`) run this
|
|
48
|
+
// returns only the selected post-mount services so we emit per-service
|
|
49
|
+
// resource files for those alone.
|
|
50
|
+
const mountGroups = scopedMountGroups(ctx);
|
|
49
51
|
const entries: Array<{ name: string; operations: Operation[] }> =
|
|
50
|
-
mountGroups.size > 0
|
|
52
|
+
mountGroups.size > 0 || ctx.scopedServices?.size
|
|
51
53
|
? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
|
|
52
54
|
: services.map((s) => ({ name: className(s.name), operations: s.operations }));
|
|
53
55
|
|