@workos/oagen-emitters 0.18.4 → 0.19.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 +14 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{plugin-Cciic50q.mjs → plugin-DXIciTnN.mjs} +668 -164
- package/dist/plugin-DXIciTnN.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/fixtures.ts +28 -7
- package/src/dotnet/index.ts +42 -1
- package/src/dotnet/models.ts +11 -5
- package/src/dotnet/resources.ts +3 -3
- package/src/dotnet/tests.ts +4 -4
- package/src/go/enums.ts +91 -18
- package/src/go/fixtures.ts +25 -3
- package/src/go/flat-merge.ts +253 -0
- package/src/go/models.ts +85 -20
- package/src/go/resources.ts +3 -3
- package/src/go/tests.ts +7 -5
- package/src/kotlin/enums.ts +21 -11
- package/src/kotlin/models.ts +53 -11
- package/src/kotlin/resources.ts +2 -2
- package/src/kotlin/tests.ts +38 -3
- 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/fixtures.ts +34 -6
- package/src/python/models.ts +138 -45
- package/src/python/resources.ts +3 -3
- package/src/python/tests.ts +31 -12
- 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 +37 -4
- package/src/rust/enums.ts +29 -7
- package/src/rust/fixtures.ts +12 -3
- package/src/rust/models.ts +37 -6
- package/src/rust/resources.ts +8 -1
- package/src/rust/tests.ts +3 -3
- package/src/shared/resolved-ops.ts +104 -0
- package/test/dotnet/scoped-aggregates.test.ts +247 -0
- package/test/go/scoping.test.ts +324 -0
- package/test/kotlin/models.test.ts +74 -0
- package/test/kotlin/tests.test.ts +33 -0
- package/test/python/scoped-aggregates.test.ts +205 -0
- package/test/ruby/tests.test.ts +130 -0
- package/test/rust/fixtures.test.ts +13 -7
- 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-DXIciTnN.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.1",
|
|
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/fixtures.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import type { Model, TypeRef, Enum } from '@workos/oagen';
|
|
1
|
+
import type { Model, TypeRef, Enum, EmitterContext } from '@workos/oagen';
|
|
2
2
|
import { fixtureFileName, domainFieldName } from './naming.js';
|
|
3
3
|
import { isListMetadataModel, isListWrapperModel } from './models.js';
|
|
4
4
|
import { collectNonPaginatedResponseModelNames } from '../shared/model-utils.js';
|
|
5
|
+
import { isModelInScope, fileExistsAfterRun } from '../shared/resolved-ops.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Prefix the per-model fixture path with the .NET test project layout so the
|
|
9
|
+
* scoped-run gate can compare it against the prefixed paths recorded in the
|
|
10
|
+
* prior manifest. Must mirror `TEST_PREFIX` in index.ts; the fixture path
|
|
11
|
+
* itself (`testdata/<name>.json`) is what `prefixTestPaths` later prepends to.
|
|
12
|
+
*/
|
|
13
|
+
const TEST_PREFIX = 'test/WorkOSTests/';
|
|
5
14
|
|
|
6
15
|
/**
|
|
7
16
|
* Prefix mapping for generating realistic ID fixture values.
|
|
@@ -25,11 +34,14 @@ export const ID_PREFIXES: Record<string, string> = {
|
|
|
25
34
|
/**
|
|
26
35
|
* Generate JSON fixture files for test data.
|
|
27
36
|
*/
|
|
28
|
-
export function generateFixtures(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
export function generateFixtures(
|
|
38
|
+
spec: {
|
|
39
|
+
models: Model[];
|
|
40
|
+
enums: Enum[];
|
|
41
|
+
services: any[];
|
|
42
|
+
},
|
|
43
|
+
ctx: EmitterContext,
|
|
44
|
+
): { path: string; content: string }[] {
|
|
33
45
|
if (spec.models.length === 0) return [];
|
|
34
46
|
|
|
35
47
|
const modelMap = new Map(spec.models.map((m) => [m.name, m]));
|
|
@@ -50,10 +62,19 @@ export function generateFixtures(spec: {
|
|
|
50
62
|
if (isListMetadataModel(model)) continue;
|
|
51
63
|
if (isListWrapperModel(model) && !nonPaginatedWrapperRefs.has(model.name)) continue;
|
|
52
64
|
|
|
65
|
+
// Scoped-run gate: only emit a per-model fixture whose file will exist on
|
|
66
|
+
// disk after the run (in-scope, or already present from a prior manifest).
|
|
67
|
+
// A brand-new out-of-scope model's fixture would be a stray file for a
|
|
68
|
+
// model no in-scope test can reference; a renamed/removed-but-on-disk one
|
|
69
|
+
// is retained. Full runs emit everything. `relPath` carries the
|
|
70
|
+
// `test/WorkOSTests/` prefix so it matches the prefixed prior-manifest paths.
|
|
71
|
+
const fixturePath = `testdata/${fixtureFileName(model.name)}.json`;
|
|
72
|
+
if (!fileExistsAfterRun(`${TEST_PREFIX}${fixturePath}`, isModelInScope(model.name, ctx), ctx)) continue;
|
|
73
|
+
|
|
53
74
|
const fixture = model.fields.length === 0 ? {} : generateModelFixture(model, modelMap, enumMap);
|
|
54
75
|
|
|
55
76
|
files.push({
|
|
56
|
-
path:
|
|
77
|
+
path: fixturePath,
|
|
57
78
|
content: JSON.stringify(fixture, null, 2),
|
|
58
79
|
});
|
|
59
80
|
|
package/src/dotnet/index.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { generateClient } from './client.js';
|
|
|
19
19
|
import { generateTests } from './tests.js';
|
|
20
20
|
import { buildOperationsMap } from './manifest.js';
|
|
21
21
|
import { generateWrapperOptionsClasses } from './wrappers.js';
|
|
22
|
-
import { groupByMount } from '../shared/resolved-ops.js';
|
|
22
|
+
import { groupByMount, isModelInScope, fileExistsAfterRun } from '../shared/resolved-ops.js';
|
|
23
23
|
import { AUTOGEN_NOTICE } from '../shared/file-header.js';
|
|
24
24
|
import { discriminatedUnions, resolveModelName } from './type-map.js';
|
|
25
25
|
import { modelClassName } from './naming.js';
|
|
@@ -66,6 +66,28 @@ function prefixTestPaths(files: GeneratedFile[]): GeneratedFile[] {
|
|
|
66
66
|
return files;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Scoped-run gate for a discriminated-union variant referenced by a generated
|
|
71
|
+
* converter (the event-registry / polymorphic dispatch aggregate). A converter
|
|
72
|
+
* may only name a variant whose per-model `Entities/<Class>.cs` file will exist
|
|
73
|
+
* on disk after the run — otherwise the `new <Class>()` / `ToObject<<Class>>`
|
|
74
|
+
* arm references a type whose file is never emitted (CS0246). The set that
|
|
75
|
+
* exists after the run is: in-scope variants (emitted this run) ∪ variants
|
|
76
|
+
* already on disk from a prior manifest (scoped runs never prune), so a
|
|
77
|
+
* renamed/removed-but-still-on-disk variant is RETAINED while a brand-new
|
|
78
|
+
* out-of-scope variant is EXCLUDED. A full run includes everything.
|
|
79
|
+
*
|
|
80
|
+
* The variant name is resolved through the model-alias map first, exactly as
|
|
81
|
+
* the converter emits it (`modelClassName(resolveModelName(name))`), and the
|
|
82
|
+
* candidate path carries the `src/WorkOS.net/` prefix so it matches the
|
|
83
|
+
* prefixed paths recorded in `priorTargetManifestPaths`.
|
|
84
|
+
*/
|
|
85
|
+
function variantFileExistsAfterRun(variantModelName: string, ctx: EmitterContext): boolean {
|
|
86
|
+
const canonical = resolveModelName(variantModelName);
|
|
87
|
+
const relPath = `${SRC_PREFIX}Entities/${modelClassName(canonical)}.cs`;
|
|
88
|
+
return fileExistsAfterRun(relPath, isModelInScope(canonical, ctx), ctx);
|
|
89
|
+
}
|
|
90
|
+
|
|
69
91
|
export const dotnetEmitter: Emitter = {
|
|
70
92
|
language: 'dotnet',
|
|
71
93
|
|
|
@@ -146,6 +168,11 @@ export const dotnetEmitter: Emitter = {
|
|
|
146
168
|
lines.push(' switch (discriminatorValue)');
|
|
147
169
|
lines.push(' {');
|
|
148
170
|
for (const [value, modelName] of Object.entries(disc.mapping)) {
|
|
171
|
+
// Scoped-run gate: only dispatch to a variant whose `Entities/*.cs`
|
|
172
|
+
// file will exist after the run. A brand-new out-of-scope variant is
|
|
173
|
+
// skipped (its `.cs` is never emitted ⇒ CS0246); unmatched discriminator
|
|
174
|
+
// values fall through to the `default` arm below.
|
|
175
|
+
if (!variantFileExistsAfterRun(modelName, c)) continue;
|
|
149
176
|
const csName = modelClassName(resolveModelName(modelName));
|
|
150
177
|
lines.push(` case "${value}": return jObject.ToObject<${csName}>(serializer);`);
|
|
151
178
|
}
|
|
@@ -177,6 +204,12 @@ export const dotnetEmitter: Emitter = {
|
|
|
177
204
|
// CanWrite is false so serialization uses the default path and never
|
|
178
205
|
// re-enters WriteJson.
|
|
179
206
|
for (const [baseName, disc] of modelDiscriminators) {
|
|
207
|
+
// Skip the converter entirely when the base model's own `Entities/*.cs`
|
|
208
|
+
// won't exist after a scoped run (brand-new out-of-scope base): its
|
|
209
|
+
// `new <baseClass>()` default arm and `[JsonConverter]` attribute would
|
|
210
|
+
// both dangle. When the base is already on disk we still emit it — the
|
|
211
|
+
// untouched base class references this converter by name.
|
|
212
|
+
if (!variantFileExistsAfterRun(baseName, c)) continue;
|
|
180
213
|
const baseClass = modelClassName(baseName);
|
|
181
214
|
const converterName = `${baseClass}DiscriminatorConverter`;
|
|
182
215
|
const lines: string[] = [];
|
|
@@ -210,6 +243,14 @@ export const dotnetEmitter: Emitter = {
|
|
|
210
243
|
lines.push(' switch (discriminatorValue)');
|
|
211
244
|
lines.push(' {');
|
|
212
245
|
for (const [value, variantModelName] of Object.entries(disc.mapping)) {
|
|
246
|
+
// Scoped-run gate: only construct a variant whose `Entities/*.cs` file
|
|
247
|
+
// will exist after the run. This is the event-registry aggregate
|
|
248
|
+
// (`EventSchema` enumerates every event payload model): a brand-new
|
|
249
|
+
// out-of-scope event type is skipped (its `.cs` is never emitted ⇒
|
|
250
|
+
// CS0246), while a renamed/removed-but-still-on-disk one is retained.
|
|
251
|
+
// Unmatched discriminator values fall through to the `default` arm,
|
|
252
|
+
// which deserializes into the base class.
|
|
253
|
+
if (!variantFileExistsAfterRun(variantModelName, c)) continue;
|
|
213
254
|
const csName = modelClassName(variantModelName);
|
|
214
255
|
lines.push(` case "${value}": target = new ${csName}(); break;`);
|
|
215
256
|
}
|
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,
|
|
@@ -30,7 +30,7 @@ export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
30
30
|
const files: GeneratedFile[] = [];
|
|
31
31
|
|
|
32
32
|
// Generate fixture JSON files
|
|
33
|
-
const fixtures = generateFixtures(spec);
|
|
33
|
+
const fixtures = generateFixtures(spec, ctx);
|
|
34
34
|
for (const fixture of fixtures) {
|
|
35
35
|
files.push({
|
|
36
36
|
path: fixture.path,
|
|
@@ -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/enums.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Enum, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
|
|
2
2
|
import { walkTypeRef } from '@workos/oagen';
|
|
3
3
|
import { className } from './naming.js';
|
|
4
|
+
import { isEnumInScope, isScopedRun } from '../shared/resolved-ops.js';
|
|
5
|
+
import { reconcileFlatBlocks, readPriorFile, type NamedBlock } from './flat-merge.js';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Generate Go typed string enum constants from IR Enum definitions.
|
|
@@ -16,6 +18,16 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
16
18
|
if (enums.length === 0) return [];
|
|
17
19
|
|
|
18
20
|
const aliasOf = collectEnumAliasOf(enums);
|
|
21
|
+
// An in-scope enum alias emits `type Alias = Canonical` against the current
|
|
22
|
+
// spec; the canonical may itself be out of scope and brand-new, which the
|
|
23
|
+
// reconciler would drop, leaving the alias dangling. Force-retain any
|
|
24
|
+
// canonical referenced by an in-scope alias.
|
|
25
|
+
const forcedCanonicals = new Set<string>();
|
|
26
|
+
if (isScopedRun(ctx)) {
|
|
27
|
+
for (const [aliasName, canonical] of aliasOf) {
|
|
28
|
+
if (isEnumInScope(aliasName, ctx)) forcedCanonicals.add(canonical);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
19
31
|
const files: GeneratedFile[] = [];
|
|
20
32
|
|
|
21
33
|
// Group all enums into a single file per SDK
|
|
@@ -23,6 +35,12 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
23
35
|
lines.push(`package ${ctx.namespace}`);
|
|
24
36
|
lines.push('');
|
|
25
37
|
|
|
38
|
+
// Build one NamedBlock per emitted enum type so a scoped run can reconcile
|
|
39
|
+
// them against the prior enums.go (drop brand-new out-of-scope enums, retain
|
|
40
|
+
// renamed/removed ones still referenced by un-regenerated code). A full run
|
|
41
|
+
// emits every block unchanged.
|
|
42
|
+
const enumBlocks: NamedBlock[] = [];
|
|
43
|
+
|
|
26
44
|
for (const enumDef of enums) {
|
|
27
45
|
// If this enum is an alias, emit a simple type alias
|
|
28
46
|
const canonicalName = aliasOf.get(enumDef.name);
|
|
@@ -33,9 +51,11 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
33
51
|
// enums from enrichModelsFromSpec whose underscore names collapse to the
|
|
34
52
|
// same PascalCase as the original enum).
|
|
35
53
|
if (aliasType === canonicalType) continue;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
enumBlocks.push({
|
|
55
|
+
names: [aliasType],
|
|
56
|
+
text: [`// ${aliasType} is an alias for ${canonicalType}.`, `type ${aliasType} = ${canonicalType}`].join('\n'),
|
|
57
|
+
inScope: isEnumInScope(enumDef.name, ctx),
|
|
58
|
+
});
|
|
39
59
|
continue;
|
|
40
60
|
}
|
|
41
61
|
|
|
@@ -43,9 +63,11 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
43
63
|
|
|
44
64
|
if (enumDef.values.length === 0) {
|
|
45
65
|
const humanized = humanize(enumDef.name);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
66
|
+
enumBlocks.push({
|
|
67
|
+
names: [typeName],
|
|
68
|
+
text: [`// ${typeName} represents ${humanized} values.`, `type ${typeName} = string`].join('\n'),
|
|
69
|
+
inScope: isEnumInScope(enumDef.name, ctx) || forcedCanonicals.has(enumDef.name),
|
|
70
|
+
});
|
|
49
71
|
continue;
|
|
50
72
|
}
|
|
51
73
|
|
|
@@ -61,10 +83,11 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
61
83
|
}
|
|
62
84
|
|
|
63
85
|
const humanized = humanize(enumDef.name);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
const blockLines: string[] = [];
|
|
87
|
+
blockLines.push(`// ${typeName} represents ${humanized} values.`);
|
|
88
|
+
blockLines.push(`type ${typeName} string`);
|
|
89
|
+
blockLines.push('');
|
|
90
|
+
blockLines.push('const (');
|
|
68
91
|
|
|
69
92
|
const usedNames = new Set<string>();
|
|
70
93
|
for (const v of uniqueValues) {
|
|
@@ -79,15 +102,25 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
79
102
|
usedNames.add(constName);
|
|
80
103
|
const valueStr = typeof v.value === 'string' ? `"${v.value}"` : String(v.value);
|
|
81
104
|
if (v.description) {
|
|
82
|
-
|
|
105
|
+
blockLines.push(`\t// ${constName} is ${v.description}.`);
|
|
83
106
|
}
|
|
84
107
|
if (v.deprecated) {
|
|
85
|
-
if (v.description)
|
|
86
|
-
|
|
108
|
+
if (v.description) blockLines.push(`\t//`);
|
|
109
|
+
blockLines.push(`\t// Deprecated: this value is deprecated.`);
|
|
87
110
|
}
|
|
88
|
-
|
|
111
|
+
blockLines.push(`\t${constName} ${typeName} = ${valueStr}`);
|
|
89
112
|
}
|
|
90
|
-
|
|
113
|
+
blockLines.push(')');
|
|
114
|
+
enumBlocks.push({
|
|
115
|
+
names: [typeName],
|
|
116
|
+
text: blockLines.join('\n'),
|
|
117
|
+
inScope: isEnumInScope(enumDef.name, ctx) || forcedCanonicals.has(enumDef.name),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const reconciled = reconcileFlatBlocks(enumBlocks, 'enums.go', ctx);
|
|
122
|
+
for (const text of reconciled) {
|
|
123
|
+
lines.push(text);
|
|
91
124
|
lines.push('');
|
|
92
125
|
}
|
|
93
126
|
|
|
@@ -96,7 +129,7 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
96
129
|
content: lines.join('\n'),
|
|
97
130
|
overwriteExisting: true,
|
|
98
131
|
});
|
|
99
|
-
const eventConstantsFile = generateEventConstantsFile(enums);
|
|
132
|
+
const eventConstantsFile = generateEventConstantsFile(enums, ctx);
|
|
100
133
|
if (eventConstantsFile) files.push(eventConstantsFile);
|
|
101
134
|
|
|
102
135
|
return files;
|
|
@@ -175,10 +208,28 @@ function collectEnumAliasOf(enums: Enum[]): Map<string, string> {
|
|
|
175
208
|
return aliasOf;
|
|
176
209
|
}
|
|
177
210
|
|
|
178
|
-
|
|
211
|
+
const EVENTS_FILE_PATH = 'pkg/events/events.go';
|
|
212
|
+
|
|
213
|
+
function generateEventConstantsFile(enums: Enum[], ctx: EmitterContext): GeneratedFile | null {
|
|
179
214
|
const enumDef = findWebhookEventEnum(enums);
|
|
180
215
|
if (!enumDef) return null;
|
|
181
216
|
|
|
217
|
+
// Scoped-run gate for the flat events file. Unlike models/enums, an event
|
|
218
|
+
// value can't be mapped to a single selected service, so there is no
|
|
219
|
+
// per-event "in scope" signal. The correct Option-B behavior is therefore to
|
|
220
|
+
// keep this aggregate byte-stable in a scoped run: emit only event constants
|
|
221
|
+
// that already existed in the prior events.go (dropping brand-new additions
|
|
222
|
+
// such as `session.reauthenticated` from an out-of-scope service) and never
|
|
223
|
+
// drop a constant that was present before. Equivalent to: in a scoped run,
|
|
224
|
+
// emit exactly the prior file's constant set.
|
|
225
|
+
let priorEventConsts: Set<string> | null = null;
|
|
226
|
+
if (isScopedRun(ctx)) {
|
|
227
|
+
const prior = readPriorFile(EVENTS_FILE_PATH, ctx);
|
|
228
|
+
if (prior !== null) {
|
|
229
|
+
priorEventConsts = collectPriorEventConstNames(prior);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
182
233
|
const lines: string[] = [];
|
|
183
234
|
lines.push('package events');
|
|
184
235
|
lines.push('');
|
|
@@ -195,6 +246,13 @@ function generateEventConstantsFile(enums: Enum[]): GeneratedFile | null {
|
|
|
195
246
|
seenValues.add(valueStr);
|
|
196
247
|
|
|
197
248
|
const constName = uniqueEventConstantName(valueStr, usedNames);
|
|
249
|
+
|
|
250
|
+
// Scoped run: skip a constant that wasn't in the prior file (a brand-new,
|
|
251
|
+
// out-of-scope event). When there's no prior file to compare against, fall
|
|
252
|
+
// through and emit everything (first generation / non-target run).
|
|
253
|
+
if (priorEventConsts !== null && !priorEventConsts.has(constName)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
198
256
|
usedNames.add(constName);
|
|
199
257
|
|
|
200
258
|
if (value.description) {
|
|
@@ -213,12 +271,27 @@ function generateEventConstantsFile(enums: Enum[]): GeneratedFile | null {
|
|
|
213
271
|
lines.push('');
|
|
214
272
|
|
|
215
273
|
return {
|
|
216
|
-
path:
|
|
274
|
+
path: EVENTS_FILE_PATH,
|
|
217
275
|
content: lines.join('\n'),
|
|
218
276
|
overwriteExisting: true,
|
|
219
277
|
};
|
|
220
278
|
}
|
|
221
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Collect the constant names declared in a prior `pkg/events/events.go`. Each
|
|
282
|
+
* event constant is emitted as `\tConstName = "wire.value"` inside the single
|
|
283
|
+
* `const (...)` block, so a simple line scan recovers the prior name set used
|
|
284
|
+
* to keep the file byte-stable across scoped runs.
|
|
285
|
+
*/
|
|
286
|
+
function collectPriorEventConstNames(content: string): Set<string> {
|
|
287
|
+
const names = new Set<string>();
|
|
288
|
+
for (const raw of content.split('\n')) {
|
|
289
|
+
const m = raw.trim().match(/^(\w+)\s*=\s*"/);
|
|
290
|
+
if (m) names.add(m[1]);
|
|
291
|
+
}
|
|
292
|
+
return names;
|
|
293
|
+
}
|
|
294
|
+
|
|
222
295
|
function findWebhookEventEnum(enums: Enum[]): Enum | null {
|
|
223
296
|
return (
|
|
224
297
|
enums.find((enumDef) => enumDef.name === 'CreateWebhookEndpointEvents') ??
|
package/src/go/fixtures.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { Model, TypeRef, Enum } from '@workos/oagen';
|
|
1
|
+
import type { Model, TypeRef, Enum, EmitterContext } from '@workos/oagen';
|
|
2
2
|
import { fileName, domainFieldName } from './naming.js';
|
|
3
3
|
import { isListMetadataModel, isListWrapperModel } from './models.js';
|
|
4
4
|
import { collectNonPaginatedResponseModelNames, collectReferencedListMetadataModels } from '../shared/model-utils.js';
|
|
5
|
+
import { isModelInScope, fileExistsAfterRun } from '../shared/resolved-ops.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Prefix mapping for generating realistic ID fixture values.
|
|
@@ -24,8 +25,18 @@ export const ID_PREFIXES: Record<string, string> = {
|
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Generate JSON fixture files for test data.
|
|
28
|
+
*
|
|
29
|
+
* Scoped runs only emit a fixture for a model whose per-model fixture FILE will
|
|
30
|
+
* exist on disk after the run (in-scope, or already present from a prior run —
|
|
31
|
+
* the fixture path is per-model so the prior manifest records it directly).
|
|
32
|
+
* This drops brand-new out-of-scope fixtures (the round-trip ADDITION case) and
|
|
33
|
+
* keeps prior fixtures untouched, mirroring the Rust fixtures fix. `ctx` is
|
|
34
|
+
* optional so unit tests that assert raw content can call it for a full run.
|
|
27
35
|
*/
|
|
28
|
-
export function generateFixtures(
|
|
36
|
+
export function generateFixtures(
|
|
37
|
+
spec: { models: Model[]; enums: Enum[]; services: any[] },
|
|
38
|
+
ctx?: EmitterContext,
|
|
39
|
+
): {
|
|
29
40
|
files: { path: string; content: string }[];
|
|
30
41
|
pathRewrites: Map<string, string>;
|
|
31
42
|
} {
|
|
@@ -38,14 +49,21 @@ export function generateFixtures(spec: { models: Model[]; enums: Enum[]; service
|
|
|
38
49
|
const nonPaginatedRefs = collectNonPaginatedResponseModelNames(spec.services);
|
|
39
50
|
const listMetadataNeeded = collectReferencedListMetadataModels(spec.models, nonPaginatedRefs);
|
|
40
51
|
|
|
52
|
+
// Full run (no ctx / no scoping): every fixture is in scope.
|
|
53
|
+
const fixtureInScope = (relPath: string, modelName: string): boolean =>
|
|
54
|
+
!ctx || fileExistsAfterRun(relPath, isModelInScope(modelName, ctx), ctx);
|
|
55
|
+
|
|
41
56
|
for (const model of spec.models) {
|
|
42
57
|
if (isListMetadataModel(model) && !listMetadataNeeded.has(model.name)) continue;
|
|
43
58
|
if (isListWrapperModel(model) && !nonPaginatedRefs.has(model.name)) continue;
|
|
44
59
|
|
|
60
|
+
const path = `testdata/${fileName(model.name)}.json`;
|
|
61
|
+
if (!fixtureInScope(path, model.name)) continue;
|
|
62
|
+
|
|
45
63
|
const fixture = model.fields.length === 0 ? {} : generateModelFixture(model, modelMap, enumMap);
|
|
46
64
|
|
|
47
65
|
files.push({
|
|
48
|
-
path
|
|
66
|
+
path,
|
|
49
67
|
content: JSON.stringify(fixture, null, 2),
|
|
50
68
|
});
|
|
51
69
|
}
|
|
@@ -67,6 +85,10 @@ export function generateFixtures(spec: { models: Model[]; enums: Enum[]; service
|
|
|
67
85
|
const path = `testdata/list_${fileName(itemModel.name)}.json`;
|
|
68
86
|
if (seenListPaths.has(path)) continue;
|
|
69
87
|
seenListPaths.add(path);
|
|
88
|
+
// Scoped run: only emit a list fixture whose file will exist after the
|
|
89
|
+
// run (in-scope item model, or already on disk). Keyed on the item
|
|
90
|
+
// model so an out-of-scope paginated endpoint doesn't add a new file.
|
|
91
|
+
if (!fixtureInScope(path, itemModel.name)) continue;
|
|
70
92
|
const fixture = generateModelFixture(itemModel, modelMap, enumMap);
|
|
71
93
|
const listFixture = {
|
|
72
94
|
data: [fixture],
|