@workos/oagen-emitters 0.18.2 → 0.18.3

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-bqfwowQ3.mjs";
1
+ import { t as workosEmittersPlugin } from "./plugin-1ckLMpgo.mjs";
2
2
  export { workosEmittersPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workos/oagen-emitters",
3
- "version": "0.18.2",
3
+ "version": "0.18.3",
4
4
  "description": "WorkOS' oagen emitters",
5
5
  "license": "MIT",
6
6
  "author": "WorkOS",
@@ -1,6 +1,7 @@
1
1
  import type { Model, TypeRef, Enum } from '@workos/oagen';
2
2
  import { fixtureFileName, fieldName } from './naming.js';
3
3
  import { isListMetadataModel, isListWrapperModel } from './models.js';
4
+ import { collectNonPaginatedResponseModelNames } from '../shared/model-utils.js';
4
5
 
5
6
  /**
6
7
  * Prefix mapping for generating realistic ID fixture values.
@@ -35,9 +36,19 @@ export function generateFixtures(spec: {
35
36
  const enumMap = new Map(spec.enums.map((e) => [e.name, e]));
36
37
  const files: { path: string; content: string }[] = [];
37
38
 
39
+ // List-wrappers are normally represented only by the per-operation
40
+ // `list_<item>.json` fixtures generated from paginated operations below. But
41
+ // a wrapper returned by a NON-paginated operation (e.g.
42
+ // `PUT /authorization/groups/{id}/role_assignments` -> GroupRoleAssignmentList)
43
+ // is emitted as a real model (see models.ts) and its generated test references
44
+ // `testdata/<type>.json` (tests.ts). Emit that envelope fixture too, mirroring
45
+ // the non-wrapper `VersionListResponse` precedent — otherwise the test loads a
46
+ // file that was never written.
47
+ const nonPaginatedWrapperRefs = collectNonPaginatedResponseModelNames(spec.services);
48
+
38
49
  for (const model of spec.models) {
39
50
  if (isListMetadataModel(model)) continue;
40
- if (isListWrapperModel(model)) continue;
51
+ if (isListWrapperModel(model) && !nonPaginatedWrapperRefs.has(model.name)) continue;
41
52
 
42
53
  const fixture = model.fields.length === 0 ? {} : generateModelFixture(model, modelMap, enumMap);
43
54
 
@@ -279,6 +279,24 @@ function operationHasOptionsInput(op: Operation, plan: OperationPlan, resolvedOp
279
279
  );
280
280
  }
281
281
 
282
+ // True only when the type is a SINGLE closed object literal (`{ ... }`), not
283
+ // the head of a compound type such as `{ ... } & X` or `{ ... } | Y`. Counting
284
+ // brace depth locates the literal's matching close brace (handling nesting like
285
+ // `{ a: { b: string } }`); any non-whitespace after it means the type is not a
286
+ // pure literal and must be preserved verbatim rather than replaced by a named
287
+ // request interface.
288
+ function isClosedObjectLiteral(type: string): boolean {
289
+ const t = type.trim();
290
+ if (!t.startsWith('{')) return false;
291
+ let depth = 0;
292
+ for (let i = 0; i < t.length; i++) {
293
+ const ch = t[i];
294
+ if (ch === '{') depth++;
295
+ else if (ch === '}' && --depth === 0) return i === t.length - 1;
296
+ }
297
+ return false;
298
+ }
299
+
282
300
  function optionsObjectInfo(
283
301
  service: Service,
284
302
  method: string,
@@ -300,9 +318,10 @@ function optionsObjectInfo(
300
318
  // operation owns a named request-body model, adopt that interface so the
301
319
  // method signature, the serializer, and the request model all agree.
302
320
  // Named baseline types (`CreateOrganizationApiKeyOptions`) and compound
303
- // intersections (`X & { ... }`) are still preserved verbatim.
321
+ // intersections (`X & { ... }` or `{ ... } & X`) are still preserved
322
+ // verbatim — only a single, closed object literal is eligible for adoption.
304
323
  if (
305
- baseline.type.trimStart().startsWith('{') &&
324
+ isClosedObjectLiteral(baseline.type) &&
306
325
  isNodeOwnedService(ctx, service.name, resolveResourceClassName(service, ctx))
307
326
  ) {
308
327
  const body = extractRequestBodyType(op, ctx);
@@ -921,7 +921,7 @@ describe('inline object-literal baseline parameter types', () => {
921
921
  ],
922
922
  });
923
923
 
924
- const baselineCtx = (service: Service, models: any[]): EmitterContext => ({
924
+ const baselineCtx = (service: Service, models: any[], paramType: string = literalType): EmitterContext => ({
925
925
  ...ctx,
926
926
  spec: { ...emptySpec, services: [service], models },
927
927
  emitterOptions: { ownedServices: ['AdminPortal'] },
@@ -933,7 +933,7 @@ describe('inline object-literal baseline parameter types', () => {
933
933
  generateLink: [
934
934
  {
935
935
  name: 'generateLink',
936
- params: [{ name: 'options', type: literalType, passingStyle: 'options_object' }],
936
+ params: [{ name: 'options', type: paramType, passingStyle: 'options_object' }],
937
937
  returnType: 'Promise<{ link: string }>',
938
938
  async: true,
939
939
  },
@@ -1007,6 +1007,35 @@ describe('inline object-literal baseline parameter types', () => {
1007
1007
  expect(content).toContain(`async generateLink(options: ${literalType})`);
1008
1008
  expect(content).not.toContain('import type { {');
1009
1009
  });
1010
+
1011
+ it('keeps a `{ ... } & X` compound intersection inline even with a named request model', () => {
1012
+ // The adoption guard targets a PURE object literal. A baseline param that
1013
+ // LEADS with a literal but is actually a compound intersection
1014
+ // (`{ ... } & WithMetadata`) carries the hand-authored `& X` portion, which
1015
+ // a named-interface swap would silently drop. It must be preserved verbatim
1016
+ // even though the operation has a single named request-body model.
1017
+ const compoundType = `${literalType} & WithMetadata`;
1018
+ const service = adminPortalService({ kind: 'model', name: 'GenerateLinkBody' });
1019
+ const models = [
1020
+ {
1021
+ name: 'GenerateLinkBody',
1022
+ fields: [
1023
+ { name: 'intent', type: { kind: 'primitive', type: 'string' }, required: true },
1024
+ { name: 'organization', type: { kind: 'primitive', type: 'string' }, required: true },
1025
+ ],
1026
+ },
1027
+ { name: 'PortalLink', fields: [{ name: 'link', type: { kind: 'primitive', type: 'string' }, required: true }] },
1028
+ ];
1029
+
1030
+ const result = generateResources([service], baselineCtx(service, models, compoundType));
1031
+ const content = result.find((f) => f.path === 'src/admin-portal/admin-portal.ts')!.content;
1032
+
1033
+ // The compound type (including the `& WithMetadata` tail) survives intact;
1034
+ // it is NOT replaced by the named `GenerateLinkBody` interface.
1035
+ expect(content).toContain(`async generateLink(options: ${compoundType})`);
1036
+ expect(content).not.toContain('async generateLink(options: GenerateLinkBody)');
1037
+ expect(content).not.toContain('import type { {');
1038
+ });
1010
1039
  });
1011
1040
 
1012
1041
  describe('@oagen-ignore region method filtering', () => {