@workos/oagen-emitters 0.13.0 → 0.14.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/dist/plugin.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as workosEmittersPlugin } from "./plugin-B9F2jmwy.mjs";
1
+ import { t as workosEmittersPlugin } from "./plugin-DRGwxN88.mjs";
2
2
  export { workosEmittersPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workos/oagen-emitters",
3
- "version": "0.13.0",
3
+ "version": "0.14.1",
4
4
  "description": "WorkOS' oagen emitters",
5
5
  "license": "MIT",
6
6
  "author": "WorkOS",
@@ -54,6 +54,6 @@
54
54
  "node": ">=24.10.0"
55
55
  },
56
56
  "dependencies": {
57
- "@workos/oagen": "^0.19.5"
57
+ "@workos/oagen": "^0.20.0"
58
58
  }
59
59
  }
@@ -20,7 +20,11 @@ import {
20
20
  } from './naming.js';
21
21
 
22
22
  // Import and re-export shared model detection utilities
23
- import { isListWrapperModel, isListMetadataModel } from '../shared/model-utils.js';
23
+ import {
24
+ isListWrapperModel,
25
+ isListMetadataModel,
26
+ collectNonPaginatedResponseModelNames,
27
+ } from '../shared/model-utils.js';
24
28
  export { isListWrapperModel, isListMetadataModel };
25
29
 
26
30
  /**
@@ -74,6 +78,11 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
74
78
  // (see workos-dotnet#248 with `CreateUserApiKey` /
75
79
  // `UserManagementCreateApiKeyOptions`). Skip emission for those.
76
80
  const requestBodyOnlyNames = collectRequestBodyOnlyModelNames(ctx.spec.services, models);
81
+ // Wrappers referenced as a non-paginated response (e.g. `VersionListResponse`
82
+ // for `GET /vault/v1/kv/{id}/versions`) must still be emitted — the resource
83
+ // code references them by name and the pagination iterator doesn't unwrap them.
84
+ const nonPaginatedRefs = collectNonPaginatedResponseModelNames(ctx.spec.services);
85
+ const skipAsListWrapper = (m: Model): boolean => isListWrapperModel(m) && !nonPaginatedRefs.has(m.name);
77
86
 
78
87
  // Build a lookup of base model field C# names → C# types for inheritance.
79
88
  // Variant models skip inherited fields and use `new` for type-divergent ones.
@@ -81,9 +90,12 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
81
90
  if (discCtx) {
82
91
  for (const model of models) {
83
92
  if (discCtx.discriminatorBases.has(model.name)) {
93
+ const baseClassName = modelClassName(model.name);
84
94
  const fieldMap = new Map<string, string>();
85
95
  for (const field of model.fields) {
86
- fieldMap.set(fieldName(field.name), mapTypeRef(field.type));
96
+ let csName = fieldName(field.name);
97
+ if (csName === baseClassName) csName = `${csName}Value`;
98
+ fieldMap.set(csName, mapTypeRef(field.type));
87
99
  }
88
100
  baseFieldLookup.set(model.name, fieldMap);
89
101
  }
@@ -91,7 +103,7 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
91
103
  }
92
104
 
93
105
  for (const model of models) {
94
- if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
106
+ if (skipAsListWrapper(model) || isListMetadataModel(model)) continue;
95
107
  if (requestBodyOnlyNames.has(model.name)) continue;
96
108
 
97
109
  const csClassName = modelClassName(model.name);
@@ -104,7 +116,11 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
104
116
  const fieldTypes = model.fields.map((f) => mapTypeRef(f.type));
105
117
  const needsCollections = fieldTypes.some((t) => t.startsWith('List<') || t.startsWith('Dictionary<'));
106
118
  const needsSystem = fieldTypes.some((t) => t.includes('DateTimeOffset'));
107
- const needsJsonAttrs = model.fields.some((f) => f.required && isEnumRef(f.type));
119
+ // Required enums need JsonProperty / STJS; a field whose PascalCase name
120
+ // collides with the enclosing class needs the same imports for the wire-
121
+ // name override emitted below.
122
+ const hasClassNameCollision = model.fields.some((f) => fieldName(f.name) === csClassName);
123
+ const needsJsonAttrs = hasClassNameCollision || model.fields.some((f) => f.required && isEnumRef(f.type));
108
124
 
109
125
  lines.push(`namespace ${ctx.namespacePascal}`);
110
126
  lines.push('{');
@@ -154,7 +170,14 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
154
170
  // Deduplicate fields by C# property name
155
171
  const seenFieldNames = new Set<string>();
156
172
  for (const field of model.fields) {
157
- const csFieldName = fieldName(field.name);
173
+ // CS0542: a property can't share its enclosing class's name. Spec schemas
174
+ // like `Error.error` PascalCase into `Error.Error`, so suffix with `Value`
175
+ // when that happens. Track the rename so we emit an explicit
176
+ // `[JsonProperty]` attribute below — the SnakeCaseLower naming policy
177
+ // would otherwise serialize `ErrorValue` as `error_value`, not `error`.
178
+ let csFieldName = fieldName(field.name);
179
+ const collidesWithClassName = csFieldName === csClassName;
180
+ if (collidesWithClassName) csFieldName = `${csFieldName}Value`;
158
181
  if (seenFieldNames.has(csFieldName)) continue;
159
182
  seenFieldNames.add(csFieldName);
160
183
 
@@ -234,7 +257,9 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
234
257
  }
235
258
 
236
259
  const isRequiredEnum = field.required && isEnumRef(field.type) && constInit === null;
237
- lines.push(...emitJsonPropertyAttributes(field.name, { isRequiredEnum }));
260
+ lines.push(
261
+ ...emitJsonPropertyAttributes(field.name, { isRequiredEnum, explicitWireName: collidesWithClassName }),
262
+ );
238
263
  // Discriminated-union-typed field: attach the variant-dispatching converter
239
264
  // so Newtonsoft picks the right subtype on deserialization. The converter
240
265
  // name is keyed off the first IR variant model name (matches how
@@ -156,7 +156,24 @@ export function isEnumRef(ref: TypeRef): boolean {
156
156
  * omission so the API returns a clear `missing required field` error instead
157
157
  * of a confusing 422.
158
158
  */
159
- export function emitJsonPropertyAttributes(_wireName: string, options: { isRequiredEnum?: boolean } = {}): string[] {
159
+ export function emitJsonPropertyAttributes(
160
+ wireName: string,
161
+ options: { isRequiredEnum?: boolean; explicitWireName?: boolean } = {},
162
+ ): string[] {
163
+ // When the C# property name doesn't naturally map back to the wire name via
164
+ // the SnakeCaseLower naming policy (e.g. we suffixed the property to avoid a
165
+ // CS0542 class/member collision like `Error.Error` → `Error.ErrorValue`), the
166
+ // caller pins the wire name explicitly so JSON round-trip still works.
167
+ if (options.explicitWireName) {
168
+ if (options.isRequiredEnum) {
169
+ return [
170
+ ` [JsonProperty("${wireName}", DefaultValueHandling = DefaultValueHandling.Ignore)]`,
171
+ ` [STJS.JsonIgnore(Condition = STJS.JsonIgnoreCondition.WhenWritingDefault)]`,
172
+ ` [STJS.JsonPropertyName("${wireName}")]`,
173
+ ];
174
+ }
175
+ return [` [JsonProperty("${wireName}")]`, ` [STJS.JsonPropertyName("${wireName}")]`];
176
+ }
160
177
  if (options.isRequiredEnum) {
161
178
  return [
162
179
  ` [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]`,
package/src/go/models.ts CHANGED
@@ -5,7 +5,11 @@ import { className, fieldName } from './naming.js';
5
5
  import { lowerFirstForDoc, fieldDocComment, articleFor } from '../shared/naming-utils.js';
6
6
 
7
7
  // Import and re-export shared model detection utilities
8
- import { isListWrapperModel, isListMetadataModel } from '../shared/model-utils.js';
8
+ import {
9
+ isListWrapperModel,
10
+ isListMetadataModel,
11
+ collectNonPaginatedResponseModelNames,
12
+ } from '../shared/model-utils.js';
9
13
  export { isListWrapperModel, isListMetadataModel };
10
14
 
11
15
  /**
@@ -83,12 +87,17 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
83
87
  lines.push('');
84
88
 
85
89
  const requestBodyOnly = collectRequestBodyOnlyModelNames(ctx.spec.services, models);
90
+ // Wrappers referenced as a non-paginated response (e.g. `VersionListResponse`
91
+ // for `GET /vault/v1/kv/{id}/versions`) must still be emitted — the resource
92
+ // code references them by name and the pagination iterator doesn't unwrap them.
93
+ const nonPaginatedRefs = collectNonPaginatedResponseModelNames(ctx.spec.services);
94
+ const skipAsListWrapper = (m: Model): boolean => isListWrapperModel(m) && !nonPaginatedRefs.has(m.name);
86
95
 
87
96
  // Build structural hash for deduplication
88
97
  const modelHashMap = new Map<string, string>();
89
98
  const hashGroups = new Map<string, string[]>();
90
99
  for (const model of models) {
91
- if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
100
+ if (skipAsListWrapper(model) || isListMetadataModel(model)) continue;
92
101
  if (requestBodyOnly.has(model.name)) continue;
93
102
  const hash = structuralHash(model);
94
103
  modelHashMap.set(model.name, hash);
@@ -112,7 +121,7 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
112
121
 
113
122
  const batchedAliases = new Set<string>();
114
123
  for (const model of models) {
115
- if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
124
+ if (skipAsListWrapper(model) || isListMetadataModel(model)) continue;
116
125
  if (requestBodyOnly.has(model.name)) continue;
117
126
 
118
127
  const structName = className(model.name);
@@ -2,7 +2,11 @@ import type { Model, EmitterContext, GeneratedFile, TypeRef, Field } from '@work
2
2
  import { mapTypeRef, discriminatedUnions } from './type-map.js';
3
3
  import { className, propertyName, ktStringLiteral, humanize } from './naming.js';
4
4
  import { enumCanonicalMap } from './enums.js';
5
- import { isListWrapperModel, isListMetadataModel } from '../shared/model-utils.js';
5
+ import {
6
+ isListWrapperModel,
7
+ isListMetadataModel,
8
+ collectNonPaginatedResponseModelNames,
9
+ } from '../shared/model-utils.js';
6
10
 
7
11
  const KOTLIN_SRC_PREFIX = 'src/main/kotlin/';
8
12
  const MODELS_PACKAGE = 'com.workos.models';
@@ -47,7 +51,7 @@ function promoteFieldType(f: Field): Field {
47
51
  * List wrappers (`{ data, list_metadata }`) and the shared `ListMetadata`
48
52
  * model are skipped — the hand-maintained runtime provides [Page]/[ListMetadata].
49
53
  */
50
- export function generateModels(models: Model[], _ctx: EmitterContext): GeneratedFile[] {
54
+ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[] {
51
55
  if (models.length === 0) return [];
52
56
 
53
57
  // First pass: call mapTypeRef on every model field so discriminator info is
@@ -58,12 +62,18 @@ export function generateModels(models: Model[], _ctx: EmitterContext): Generated
58
62
 
59
63
  const files: GeneratedFile[] = [];
60
64
 
65
+ // Wrappers referenced as a non-paginated response (e.g. `VersionListResponse`
66
+ // for `GET /vault/v1/kv/{id}/versions`) must still be emitted — the resource
67
+ // code references them by name and pagination iterators don't unwrap them.
68
+ const nonPaginatedRefs = collectNonPaginatedResponseModelNames(ctx.spec.services);
69
+ const skipAsListWrapper = (m: Model): boolean => isListWrapperModel(m) && !nonPaginatedRefs.has(m.name);
70
+
61
71
  // Deduplication: identical structures become typealiases.
62
72
  // Pass 1: hash without nested-alias resolution.
63
73
  modelAliasMap = null;
64
74
  const hashGroupsPass1 = new Map<string, string[]>();
65
75
  for (const model of models) {
66
- if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
76
+ if (skipAsListWrapper(model) || isListMetadataModel(model)) continue;
67
77
  if (model.fields.length === 0 && discriminatedUnions.has(className(model.name))) continue;
68
78
  const hash = structuralHash(model);
69
79
  if (!hashGroupsPass1.has(hash)) hashGroupsPass1.set(hash, []);
@@ -86,7 +96,7 @@ export function generateModels(models: Model[], _ctx: EmitterContext): Generated
86
96
  modelAliasMap = aliasOf;
87
97
  const hashGroupsPass2 = new Map<string, string[]>();
88
98
  for (const model of models) {
89
- if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
99
+ if (skipAsListWrapper(model) || isListMetadataModel(model)) continue;
90
100
  if (model.fields.length === 0 && discriminatedUnions.has(className(model.name))) continue;
91
101
  if (aliasOf.has(model.name)) continue; // already aliased in pass 1
92
102
  const hash = structuralHash(model);
@@ -105,7 +115,7 @@ export function generateModels(models: Model[], _ctx: EmitterContext): Generated
105
115
  }
106
116
 
107
117
  for (const model of models) {
108
- if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
118
+ if (skipAsListWrapper(model) || isListMetadataModel(model)) continue;
109
119
  const typeName = className(model.name);
110
120
 
111
121
  // Parent of a discriminated union: emit a sealed class.
@@ -142,7 +152,7 @@ export function generateModels(models: Model[], _ctx: EmitterContext): Generated
142
152
  // mapping so Jackson can deserialize directly to the correct concrete type.
143
153
  const eventMapping: Array<{ wireValue: string; modelName: string }> = [];
144
154
  for (const model of models) {
145
- if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
155
+ if (skipAsListWrapper(model) || isListMetadataModel(model)) continue;
146
156
  if (aliasOf.has(model.name)) continue;
147
157
  if (!isEventEnvelopeModel(model)) continue;
148
158
  const eventField = model.fields.find((f) => f.name === 'event');