@workos/oagen-emitters 0.8.0 → 0.8.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-bCMdV7KX.mjs";
1
+ import { t as workosEmittersPlugin } from "./plugin-DOE0FqrZ.mjs";
2
2
  export { workosEmittersPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workos/oagen-emitters",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "WorkOS' oagen emitters",
5
5
  "license": "MIT",
6
6
  "author": "WorkOS",
@@ -133,8 +133,11 @@ export const dotnetEmitter: Emitter = {
133
133
  lines.push(' {');
134
134
  lines.push(' public override bool CanConvert(Type objectType) => objectType == typeof(object);');
135
135
  lines.push('');
136
+ // Override returns `object?` to match Newtonsoft.Json 13+'s nullable
137
+ // signature; `JToken.ToObject<T>` is itself `T?`, so a non-nullable
138
+ // override would trigger CS8603 under <Nullable>enable</Nullable>.
136
139
  lines.push(
137
- ' public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object? existingValue, Newtonsoft.Json.JsonSerializer serializer)',
140
+ ' public override object? ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object? existingValue, Newtonsoft.Json.JsonSerializer serializer)',
138
141
  );
139
142
  lines.push(' {');
140
143
  lines.push(' var jObject = JObject.Load(reader);');
@@ -194,8 +197,9 @@ export const dotnetEmitter: Emitter = {
194
197
  ` public override bool CanConvert(Type objectType) => typeof(${baseClass}).IsAssignableFrom(objectType);`,
195
198
  );
196
199
  lines.push('');
200
+ // See first converter — `object?` matches Newtonsoft 13+ to avoid CS8603.
197
201
  lines.push(
198
- ' public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object? existingValue, Newtonsoft.Json.JsonSerializer serializer)',
202
+ ' public override object? ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object? existingValue, Newtonsoft.Json.JsonSerializer serializer)',
199
203
  );
200
204
  lines.push(' {');
201
205
  lines.push(' var jObject = JObject.Load(reader);');
@@ -7,6 +7,7 @@ import {
7
7
  emitJsonPropertyAttributes,
8
8
  setModelAliases,
9
9
  isModelAlias,
10
+ resolveModelName,
10
11
  } from './type-map.js';
11
12
  import {
12
13
  articleFor,
@@ -54,6 +55,17 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
54
55
  }
55
56
  }
56
57
 
58
+ const files: GeneratedFile[] = [];
59
+
60
+ // Compute and publish model aliases so mapTypeRef rewrites references.
61
+ // Must run BEFORE collectRequestBodyOnlyModelNames so the body/non-body
62
+ // tally collapses aliased pairs onto their canonical name — otherwise a
63
+ // model that's only a request body in name (e.g. `AddRolePermissionDto`)
64
+ // but is the canonical for a field-referenced alias (e.g. `SlimRole`)
65
+ // would be wrongly classified as body-only and skipped from emission,
66
+ // leaving every alias-rewritten field reference dangling.
67
+ primeModelAliases(models);
68
+
57
69
  // Models that are referenced ONLY as an operation request body (not by any
58
70
  // response, field, or other operation type) are dead surface in .NET because
59
71
  // the wrapper generator emits a per-operation `*Options` class containing
@@ -63,11 +75,6 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
63
75
  // `UserManagementCreateApiKeyOptions`). Skip emission for those.
64
76
  const requestBodyOnlyNames = collectRequestBodyOnlyModelNames(ctx.spec.services, models);
65
77
 
66
- const files: GeneratedFile[] = [];
67
-
68
- // Compute and publish model aliases so mapTypeRef rewrites references.
69
- primeModelAliases(models);
70
-
71
78
  // Build a lookup of base model field C# names → C# types for inheritance.
72
79
  // Variant models skip inherited fields and use `new` for type-divergent ones.
73
80
  const baseFieldLookup = new Map<string, Map<string, string>>();
@@ -453,17 +460,22 @@ function collectRequestBodyOnlyModelNames(services: Service[], models: Model[]):
453
460
  const requestBodyNames = new Set<string>();
454
461
  const otherReferences = new Set<string>();
455
462
 
463
+ // Resolve every reference through the alias map so structurally-identical
464
+ // models share a body/non-body classification. Without this, an alias being
465
+ // used as a field would only mark the alias name as non-body — leaving its
466
+ // canonical (which carries the same shape and gets emitted) wrongly tagged
467
+ // as body-only and skipped.
456
468
  const collect = (ref: TypeRef | undefined, into: Set<string>): void => {
457
469
  if (!ref) return;
458
470
  walkTypeRef(ref, {
459
- model: (r) => into.add(r.name),
471
+ model: (r) => into.add(resolveModelName(r.name)),
460
472
  });
461
473
  };
462
474
 
463
475
  for (const service of services) {
464
476
  for (const op of service.operations) {
465
477
  if (op.requestBody?.kind === 'model') {
466
- requestBodyNames.add(op.requestBody.name);
478
+ requestBodyNames.add(resolveModelName(op.requestBody.name));
467
479
  }
468
480
  collect(op.response, otherReferences);
469
481
  if (op.pagination) collect(op.pagination.itemType, otherReferences);
@@ -46,7 +46,12 @@ export function generateFixtures(spec: { models: Model[]; enums: Enum[]; service
46
46
  });
47
47
  }
48
48
 
49
- // Generate list fixtures for paginated responses
49
+ // Generate list fixtures for paginated responses. Multiple operations may
50
+ // share the same item model (e.g. several role-assignment list endpoints all
51
+ // returning UserRoleAssignmentList) — emit each fixture path once so the
52
+ // content-dedup pass below doesn't see N copies of the same path and drop
53
+ // the file entirely.
54
+ const seenListPaths = new Set<string>();
50
55
  for (const service of spec.services) {
51
56
  for (const op of service.operations) {
52
57
  if (op.pagination) {
@@ -55,6 +60,9 @@ export function generateFixtures(spec: { models: Model[]; enums: Enum[]; service
55
60
  const unwrapped = unwrapListModel(itemModel, modelMap);
56
61
  if (unwrapped) itemModel = unwrapped;
57
62
  if (itemModel.fields.length === 0) continue;
63
+ const path = `testdata/list_${fileName(itemModel.name)}.json`;
64
+ if (seenListPaths.has(path)) continue;
65
+ seenListPaths.add(path);
58
66
  const fixture = generateModelFixture(itemModel, modelMap, enumMap);
59
67
  const listFixture = {
60
68
  data: [fixture],
@@ -64,7 +72,7 @@ export function generateFixtures(spec: { models: Model[]; enums: Enum[]; service
64
72
  },
65
73
  };
66
74
  files.push({
67
- path: `testdata/list_${fileName(itemModel.name)}.json`,
75
+ path,
68
76
  content: JSON.stringify(listFixture, null, 2),
69
77
  });
70
78
  }
@@ -286,12 +286,10 @@ function generateServiceInits(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
286
286
  overwriteExisting: true,
287
287
  });
288
288
 
289
- // Ensure models/__init__.py exists even if no models are assigned to this service
290
- files.push({
291
- path: `src/${ctx.namespace}/${dirName}/models/__init__.py`,
292
- content: '',
293
- skipIfExists: true,
294
- });
289
+ // models/__init__.py is emitted unconditionally by `models.ts` including
290
+ // an empty barrel for services with no models — so we don't need a safety
291
+ // net here. (A `skipIfExists` safety net previously caused stale imports
292
+ // to survive when the underlying module was pruned.)
295
293
  }
296
294
 
297
295
  return files;
@@ -447,6 +447,24 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
447
447
  serviceDirModelPaths.add(`src/${ctx.namespace}/${dirName}/models`);
448
448
  }
449
449
 
450
+ // Emit an empty barrel for every service-models dir that has no symbols of
451
+ // its own (e.g. a service whose models live in another package via
452
+ // cross-domain aliases). Otherwise the live SDK can keep a stale
453
+ // `__init__.py` from a previous spec revision — when the underlying module
454
+ // gets pruned the dangling re-export survives and breaks pyright. Done here
455
+ // (not in client.ts) so a subsequent emission for the same path with real
456
+ // content always wins last-write-wins.
457
+ for (const dirPath of serviceDirModelPaths) {
458
+ if (!symbolsByDir.has(dirPath)) {
459
+ files.push({
460
+ path: `${dirPath}/__init__.py`,
461
+ content: '',
462
+ integrateTarget: true,
463
+ overwriteExisting: true,
464
+ });
465
+ }
466
+ }
467
+
450
468
  for (const [dirPath, names] of symbolsByDir) {
451
469
  // Use `import X as X` syntax for explicit re-exports (required by pyright strict)
452
470
  const uniqueNames = [...new Set(names)].sort();
@@ -719,7 +737,12 @@ function deserializeField(ref: any, accessor: string, isRequired: boolean, walru
719
737
  const dispatchMap = entries.map(([value, modelName]) => `"${value}": ${className(modelName)}`).join(', ');
720
738
  const dataExpr = isRequired ? accessor : walrusVar;
721
739
  const dataCast = `cast(Dict[str, Any], ${dataExpr})`;
722
- const lookupExpr = `{${dispatchMap}}.get(${dataCast}.get("${ref.discriminator.property}"))`;
740
+ // The dispatch dict has `str` keys, so pyright (strict) rejects the
741
+ // raw `Any | None` returned by `.get(prop)` even though `dict.get`
742
+ // accepts any hashable. Cast through `str` to satisfy the parameter
743
+ // type — runtime semantics are unchanged because a missing/`None`
744
+ // discriminator simply misses the dispatch and falls through.
745
+ const lookupExpr = `{${dispatchMap}}.get(cast(str, ${dataCast}.get("${ref.discriminator.property}")))`;
723
746
  const branch = `(_disc.from_dict(${dataCast}) if (_disc := ${lookupExpr}) is not None else ${dataExpr})`;
724
747
  if (isRequired) return branch;
725
748
  return `(${branch}) if (${walrusVar} := ${accessor}) is not None else None`;
@@ -760,6 +783,12 @@ function serializeField(ref: any, accessor: string): string {
760
783
  if (uniqueModels.length === 1) {
761
784
  return `${accessor}.to_dict()`;
762
785
  }
786
+ // Discriminated union: deserialize produced a dataclass instance for
787
+ // known discriminator values and the raw dict for unknowns. Round-trip
788
+ // both — call `.to_dict()` if it exists, otherwise pass through.
789
+ if (ref.discriminator && ref.discriminator.mapping && modelVariants.length > 0) {
790
+ return `${accessor}.to_dict() if hasattr(${accessor}, "to_dict") else ${accessor}`;
791
+ }
763
792
  return accessor;
764
793
  }
765
794
  default: