@workos/oagen-emitters 0.6.6 → 0.6.7

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-BgVrq-hM.mjs";
1
+ import { t as workosEmittersPlugin } from "./plugin-Bk0xWTQC.mjs";
2
2
  export { workosEmittersPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workos/oagen-emitters",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "WorkOS' oagen emitters",
5
5
  "license": "MIT",
6
6
  "author": "WorkOS",
@@ -103,7 +103,14 @@ export const dotnetEmitter: Emitter = {
103
103
  return m;
104
104
  });
105
105
 
106
- const discCtx: DiscriminatorContext = { discriminatorBases, variantToBase };
106
+ // Build a map of base model name → discriminator wire-property name so the
107
+ // model emitter can mark the discriminator field as internal-set.
108
+ const discriminatorProperties = new Map<string, string>();
109
+ for (const [baseName, disc] of modelDiscriminators) {
110
+ discriminatorProperties.set(baseName, disc.property);
111
+ }
112
+
113
+ const discCtx: DiscriminatorContext = { discriminatorBases, variantToBase, discriminatorProperties };
107
114
  const files = generateModels(dotnetModels, c, discCtx);
108
115
 
109
116
  // Generate discriminator converters for oneOf unions with discriminator
@@ -181,6 +188,8 @@ export const dotnetEmitter: Emitter = {
181
188
  lines.push(` /// </summary>`);
182
189
  lines.push(` public class ${converterName} : Newtonsoft.Json.JsonConverter`);
183
190
  lines.push(' {');
191
+ lines.push(' public override bool CanWrite => false;');
192
+ lines.push('');
184
193
  lines.push(
185
194
  ` public override bool CanConvert(Type objectType) => typeof(${baseClass}).IsAssignableFrom(objectType);`,
186
195
  );
@@ -206,8 +215,6 @@ export const dotnetEmitter: Emitter = {
206
215
  lines.push(' return target;');
207
216
  lines.push(' }');
208
217
  lines.push('');
209
- lines.push(' public override bool CanWrite => false;');
210
- lines.push('');
211
218
  lines.push(
212
219
  ' public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object? value, Newtonsoft.Json.JsonSerializer serializer)',
213
220
  );
@@ -31,6 +31,8 @@ export interface DiscriminatorContext {
31
31
  discriminatorBases: Set<string>;
32
32
  /** Maps variant model name → base model name. */
33
33
  variantToBase: Map<string, string>;
34
+ /** Maps base model name → wire name of the discriminator property. */
35
+ discriminatorProperties?: Map<string, string>;
34
36
  }
35
37
 
36
38
  /**
@@ -161,6 +163,12 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
161
163
  let initializer = '';
162
164
  let setterModifier = '';
163
165
 
166
+ // On a discriminated union base, the discriminator property (e.g. "event")
167
+ // should be non-public-settable even though it lacks a single const value
168
+ // (each variant has a different value). Consumers must never mutate it.
169
+ const discProp = isDiscBase ? discCtx?.discriminatorProperties?.get(model.name) : undefined;
170
+ const isDiscriminatorField = discProp !== undefined && field.name === discProp;
171
+
164
172
  if (constInit !== null && !isOptional) {
165
173
  // Discriminator-style single-value enum/literal: emit with a const
166
174
  // initializer and a non-public setter so callers can't drift the
@@ -170,6 +178,14 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
170
178
  csType = baseType;
171
179
  initializer = ` = ${constInit};`;
172
180
  setterModifier = 'internal ';
181
+ } else if (isDiscriminatorField) {
182
+ // Discriminator property on the base class: varies per variant but
183
+ // should still be non-public-settable so consumers can't change it.
184
+ csType = baseType;
185
+ if (!isAlreadyNullable && !isValueTypeRef(field.type)) {
186
+ initializer = ' = default!;';
187
+ }
188
+ setterModifier = 'internal ';
173
189
  } else if (isOptional) {
174
190
  if (isAlreadyNullable) {
175
191
  csType = baseType;
@@ -219,6 +235,14 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
219
235
  lines.push(` /// <paramref name="key"/> coerced to <typeparamref name="T"/>, or the default`);
220
236
  lines.push(` /// value when the key is missing or the value is not convertible.`);
221
237
  lines.push(` /// </summary>`);
238
+ if (isDiscBase) {
239
+ lines.push(` /// <remarks>`);
240
+ lines.push(` /// Variant subclasses provide strongly-typed <c>${dict.csName}</c> properties that`);
241
+ lines.push(` /// shadow this dictionary. This accessor is intended for forward-compatible handling`);
242
+ lines.push(` /// of types not yet known to this SDK version. For recognized types, cast to the`);
243
+ lines.push(` /// specific subclass and access its typed <c>${dict.csName}</c> property directly.`);
244
+ lines.push(` /// </remarks>`);
245
+ }
222
246
  lines.push(` /// <typeparam name="T">Expected value type.</typeparam>`);
223
247
  lines.push(` /// <param name="key">The key to look up.</param>`);
224
248
  lines.push(` public T? Get${dict.csName}Attribute<T>(string key)`);
package/src/go/models.ts CHANGED
@@ -166,10 +166,27 @@ function makeOptional(goType: string): string {
166
166
  }
167
167
 
168
168
  function structuralHash(model: Model): string {
169
- return model.fields
169
+ const fieldHash = model.fields
170
170
  .map((f) => `${f.name}:${JSON.stringify(f.type)}:${f.required}`)
171
171
  .sort()
172
172
  .join('|');
173
+ // Include entity domain for CRUD-prefixed models to prevent cross-domain
174
+ // aliasing (e.g. UpdateGroup vs UpdateAuthorizationPermission have identical
175
+ // fields but belong to different API domains and should stay separate types).
176
+ const domain = crudEntityDomain(model.name);
177
+ return domain ? `${domain}::${fieldHash}` : fieldHash;
178
+ }
179
+
180
+ const CRUD_PREFIXES = ['Create', 'Update', 'Delete', 'Get', 'List'];
181
+
182
+ /** Strip CRUD verb prefix to get the entity name, or null if no prefix matches. */
183
+ function crudEntityDomain(name: string): string | null {
184
+ for (const prefix of CRUD_PREFIXES) {
185
+ if (name.startsWith(prefix) && name.length > prefix.length) {
186
+ return name.slice(prefix.length);
187
+ }
188
+ }
189
+ return null;
173
190
  }
174
191
 
175
192
  /** Known acronyms to preserve as single tokens during humanization. */
package/src/go/tests.ts CHANGED
@@ -307,7 +307,10 @@ function generateServiceTest(
307
307
  // Success test
308
308
  const respModel = plan.responseModelName;
309
309
  const isArrayResponse = !isPaginated && op.response?.kind === 'array';
310
- const fixturePath = `testdata/${fileName(respModel)}.json`;
310
+ let fixturePath = `testdata/${fileName(respModel)}.json`;
311
+ if (fixtureRewrites.has(fixturePath)) {
312
+ fixturePath = fixtureRewrites.get(fixturePath)!;
313
+ }
311
314
  const expectedPath = buildExpectedPath(op);
312
315
 
313
316
  const httpMethodUpper = op.httpMethod.toUpperCase();
@@ -411,7 +414,10 @@ function generateServiceTest(
411
414
  const wrapperParamsStruct = paramsStructName(resolvedName, wrapperMethod);
412
415
  const responseType = wrapper.responseModelName;
413
416
  const testName = `Test${accessorName}_${wrapperMethod}`;
414
- const fixturePath = responseType ? `testdata/${fileName(responseType)}.json` : null;
417
+ let fixturePath = responseType ? `testdata/${fileName(responseType)}.json` : null;
418
+ if (fixturePath && fixtureRewrites.has(fixturePath)) {
419
+ fixturePath = fixtureRewrites.get(fixturePath)!;
420
+ }
415
421
 
416
422
  const wrapperCallArgs: string[] = ['context.Background()'];
417
423
  for (const p of sortPathParamsByTemplateOrder(op)) {
@@ -255,4 +255,81 @@ describe('dotnet/models', () => {
255
255
  expect(orgFile).toBeDefined();
256
256
  expect(orgFile.content).toContain('List<OrganizationDomain>');
257
257
  });
258
+
259
+ it('emits internal set on discriminator property of base class', () => {
260
+ const models: Model[] = [
261
+ {
262
+ name: 'EventSchema',
263
+ fields: [
264
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
265
+ { name: 'event', type: { kind: 'primitive', type: 'string' }, required: true },
266
+ {
267
+ name: 'data',
268
+ type: { kind: 'map', valueType: { kind: 'primitive', type: 'unknown' } },
269
+ required: true,
270
+ },
271
+ ],
272
+ },
273
+ {
274
+ name: 'UserCreated',
275
+ fields: [
276
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
277
+ { name: 'event', type: { kind: 'literal', value: 'user.created' }, required: true },
278
+ { name: 'data', type: { kind: 'model', name: 'UserCreatedData' }, required: true },
279
+ ],
280
+ },
281
+ {
282
+ name: 'UserCreatedData',
283
+ fields: [{ name: 'user_id', type: { kind: 'primitive', type: 'string' }, required: true }],
284
+ },
285
+ ];
286
+
287
+ primeEnumAliases([]);
288
+ const discCtx = {
289
+ discriminatorBases: new Set(['EventSchema']),
290
+ variantToBase: new Map([['UserCreated', 'EventSchema']]),
291
+ discriminatorProperties: new Map([['EventSchema', 'event']]),
292
+ };
293
+ const files = generateModels(models, { ...ctx, spec: { ...emptySpec, models } }, discCtx);
294
+
295
+ const baseFile = files.find((f) => f.path.includes('EventSchema.cs'))!;
296
+ expect(baseFile).toBeDefined();
297
+
298
+ // The discriminator property "event" should have internal set
299
+ expect(baseFile.content).toContain('Event { get; internal set; }');
300
+ // Non-discriminator required fields should NOT have internal set
301
+ expect(baseFile.content).toContain('Id { get; set; }');
302
+ });
303
+
304
+ it('adds remarks to dictionary accessors on discriminator base class', () => {
305
+ const models: Model[] = [
306
+ {
307
+ name: 'EventSchema',
308
+ fields: [
309
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
310
+ { name: 'event', type: { kind: 'primitive', type: 'string' }, required: true },
311
+ {
312
+ name: 'data',
313
+ type: { kind: 'map', valueType: { kind: 'primitive', type: 'unknown' } },
314
+ required: true,
315
+ },
316
+ ],
317
+ },
318
+ ];
319
+
320
+ primeEnumAliases([]);
321
+ const discCtx = {
322
+ discriminatorBases: new Set(['EventSchema']),
323
+ variantToBase: new Map<string, string>(),
324
+ discriminatorProperties: new Map([['EventSchema', 'event']]),
325
+ };
326
+ const files = generateModels(models, { ...ctx, spec: { ...emptySpec, models } }, discCtx);
327
+
328
+ const baseFile = files.find((f) => f.path.includes('EventSchema.cs'))!;
329
+ expect(baseFile).toBeDefined();
330
+
331
+ // Dictionary accessors on discriminator bases should have a remarks note
332
+ expect(baseFile.content).toContain('/// <remarks>');
333
+ expect(baseFile.content).toContain('forward-compatible');
334
+ });
258
335
  });