Skip to content

feat!: Generate AdminPortal from OpenAPI spec#1629

Draft
gjtorikian wants to merge 1 commit into
mainfrom
oagen/own-admin-portal
Draft

feat!: Generate AdminPortal from OpenAPI spec#1629
gjtorikian wants to merge 1 commit into
mainfrom
oagen/own-admin-portal

Conversation

@gjtorikian

Copy link
Copy Markdown
Contributor

Description

This PR generates the AdminPortal category in the Node SDK from the OpenAPI spec.

It renames one field:

Breaking — field renamed:

  ┌────────────────────────┬────────────────────────────┐
  │     Before (main)      │       After (branch)       │
  ├────────────────────────┼────────────────────────────┤
  │ adminEmails?: string[] │ itContactEmails?: string[] │
  └────────────────────────┴────────────────────────────┘

Because of this, the PR should be treated as a breaking change.

@gjtorikian gjtorikian requested review from a team as code owners June 17, 2026 01:36
@gjtorikian gjtorikian requested a review from dandorman June 17, 2026 01:36
@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR migrates the AdminPortal category to be fully generated from the OpenAPI spec via oagen, adding DomainVerification intent options support and renaming adminEmailsitContactEmails (wire: admin_emailsit_contact_emails) as a declared breaking change.

  • Breaking rename: GenerateLink.adminEmails is replaced by itContactEmails, with the serializer correctly mapping it to it_contact_emails on the wire.
  • New domain verification support: A new DomainVerificationIntentOptions interface and serializer are introduced, and IntentOptions.sso is made optional to allow other intents to be used without SSO options.
  • Test coverage reduction: The previous spec had seven intent-specific tests and error-case tests; the new spec has a single happy-path test, and no test exercises the renamed itContactEmails field or intentOptions. A generated dead file (generate-link-intent.interface.ts) is also present but never imported.

Confidence Score: 4/5

The implementation changes are correct — serializers, interfaces, and the API call are all wired up properly. The main concern is test coverage: the primary breaking change (field rename) is never exercised in any test, and all serializer tests now only check toBeDefined().

The serialization logic for the renamed itContactEmails field and the new domainVerification intent option are implemented correctly. However, the test suite was substantially simplified — round-trip assertions were dropped in favour of existence checks, and the admin-portal.spec.ts lost all intent-specific and error-case tests. A dead generated file (generate-link-intent.interface.ts) is also present but unused.

src/admin-portal/serializers.spec.ts and src/admin-portal/admin-portal.spec.ts would benefit from stronger assertions; src/admin-portal/interfaces/generate-link-intent.interface.ts should either be wired up or removed.

Important Files Changed

Filename Overview
src/admin-portal/admin-portal.ts Refactored to use generated interfaces and serializers; contains a harmless redundant intermediate variable const payload = options.
src/admin-portal/interfaces/generate-link-intent.interface.ts New file that is a duplicate of src/common/interfaces/generate-link-intent.interface.ts and is never imported anywhere — dead code left by the generator.
src/admin-portal/serializers.spec.ts Round-trip tests replaced with toBeDefined() checks that verify nothing about serialization correctness; the renamed itContactEmails field is never validated.
src/admin-portal/admin-portal.spec.ts Significantly reduced test coverage — seven intent-specific tests and an error-case test were removed, replaced with a single happy-path test that does not exercise itContactEmails or intentOptions.
src/admin-portal/serializers/generate-link.serializer.ts Correctly maps itContactEmailsit_contact_emails and removes the now-unused deserializeGenerateLink function.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller
    participant AdminPortal
    participant serializeGenerateLink
    participant serializeIntentOptions
    participant WorkOS API
    participant deserializePortalLinkResponse

    Caller->>AdminPortal: generateLink(options: GenerateLink)
    AdminPortal->>serializeGenerateLink: serializeGenerateLink(options)
    serializeGenerateLink->>serializeIntentOptions: serializeIntentOptions(model.intentOptions)
    serializeIntentOptions-->>serializeGenerateLink: IntentOptionsResponse
    serializeGenerateLink-->>AdminPortal: GenerateLinkResponse (wire format)
    AdminPortal->>WorkOS API: POST /portal/generate_link
    WorkOS API-->>AdminPortal: PortalLinkResponseWire
    AdminPortal->>deserializePortalLinkResponse: deserializePortalLinkResponse(data)
    deserializePortalLinkResponse-->>AdminPortal: PortalLinkResponse
    AdminPortal-->>Caller: "PortalLinkResponse { link }"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Caller
    participant AdminPortal
    participant serializeGenerateLink
    participant serializeIntentOptions
    participant WorkOS API
    participant deserializePortalLinkResponse

    Caller->>AdminPortal: generateLink(options: GenerateLink)
    AdminPortal->>serializeGenerateLink: serializeGenerateLink(options)
    serializeGenerateLink->>serializeIntentOptions: serializeIntentOptions(model.intentOptions)
    serializeIntentOptions-->>serializeGenerateLink: IntentOptionsResponse
    serializeGenerateLink-->>AdminPortal: GenerateLinkResponse (wire format)
    AdminPortal->>WorkOS API: POST /portal/generate_link
    WorkOS API-->>AdminPortal: PortalLinkResponseWire
    AdminPortal->>deserializePortalLinkResponse: deserializePortalLinkResponse(data)
    deserializePortalLinkResponse-->>AdminPortal: PortalLinkResponse
    AdminPortal-->>Caller: "PortalLinkResponse { link }"
Loading

Reviews (1): Last reviewed commit: "Generate AdminPortal from OpenAPI spec" | Re-trigger Greptile

Comment on lines +1 to +14
// This file is auto-generated by oagen. Do not edit.

export const GenerateLinkIntent = {
SSO: 'sso',
DSync: 'dsync',
AuditLogs: 'audit_logs',
LogStreams: 'log_streams',
DomainVerification: 'domain_verification',
CertificateRenewal: 'certificate_renewal',
BringYourOwnKey: 'bring_your_own_key',
} as const;

export type GenerateLinkIntent =
(typeof GenerateLinkIntent)[keyof typeof GenerateLinkIntent];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unused generated file — never imported

This file duplicates src/common/interfaces/generate-link-intent.interface.ts exactly, but nothing imports it. generate-link.interface.ts still imports GenerateLinkIntent from ../../common/interfaces/generate-link-intent.interface, and interfaces/index.ts does not re-export this file. The generator appears to have produced the file but did not update the import path in generate-link.interface.ts to use the local copy, leaving this file as dead code.

Comment on lines 19 to 58
describe('SSOIntentOptionsSerializer', () => {
it('round-trips through serialize/deserialize', () => {
it('serializes correctly', () => {
const fixture = ssoIntentOptionsFixture as SSOIntentOptionsResponse;
const deserialized = deserializeSSOIntentOptions(fixture);
const reserialized = serializeSSOIntentOptions(deserialized);
expect(reserialized).toEqual(expect.objectContaining(fixture));
const serialized = serializeSSOIntentOptions(fixture as any);
expect(serialized).toBeDefined();
});
});

describe('DomainVerificationIntentOptionsSerializer', () => {
it('serializes correctly', () => {
const fixture =
domainVerificationIntentOptionsFixture as DomainVerificationIntentOptionsResponse;
const serialized = serializeDomainVerificationIntentOptions(fixture as any);
expect(serialized).toBeDefined();
});
});

describe('IntentOptionsSerializer', () => {
it('round-trips through serialize/deserialize', () => {
it('serializes correctly', () => {
const fixture = intentOptionsFixture as IntentOptionsResponse;
const deserialized = deserializeIntentOptions(fixture);
const reserialized = serializeIntentOptions(deserialized);
expect(reserialized).toEqual(expect.objectContaining(fixture));
const serialized = serializeIntentOptions(fixture as any);
expect(serialized).toBeDefined();
});
});

describe('GenerateLinkSerializer', () => {
it('round-trips through serialize/deserialize', () => {
it('serializes correctly', () => {
const fixture = generateLinkFixture as GenerateLinkResponse;
const deserialized = deserializeGenerateLink(fixture);
const reserialized = serializeGenerateLink(deserialized);
expect(reserialized).toEqual(expect.objectContaining(fixture));
const serialized = serializeGenerateLink(fixture as any);
expect(serialized).toBeDefined();
});
});

describe('PortalLinkResponseSerializer', () => {
it('round-trips through serialize/deserialize', () => {
it('deserializes correctly', () => {
const fixture = portalLinkResponseFixture as PortalLinkResponseWire;
const deserialized = deserializePortalLinkResponse(fixture);
const reserialized = serializePortalLinkResponse(deserialized);
expect(reserialized).toEqual(expect.objectContaining(fixture));
expect(deserialized).toBeDefined();
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Tests only verify existence, not correctness

All serializer tests were downgraded from round-trip assertions (expect(reserialized).toEqual(expect.objectContaining(fixture))) to expect(serialized).toBeDefined(). A call that returns {} or an object where every field is undefined would still pass. Notably, the renamed field itContactEmailsit_contact_emails (the key breaking change in this PR) is never verified to round-trip correctly through any of these tests.

Comment on lines +46 to +51
async generateLink(options: GenerateLink): Promise<PortalLinkResponse> {
const payload = options;
const { data } = await this.workos.post<
PortalLinkResponseWire,
GenerateLinkResponse
>('/portal/generate_link', serializeGenerateLink(payload));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The intermediate const payload = options; assignment adds no value — options can be passed directly to serializeGenerateLink.

Suggested change
async generateLink(options: GenerateLink): Promise<PortalLinkResponse> {
const payload = options;
const { data } = await this.workos.post<
PortalLinkResponseWire,
GenerateLinkResponse
>('/portal/generate_link', serializeGenerateLink(payload));
async generateLink(options: GenerateLink): Promise<PortalLinkResponse> {
const { data } = await this.workos.post<
PortalLinkResponseWire,
GenerateLinkResponse
>('/portal/generate_link', serializeGenerateLink(options));

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@gjtorikian gjtorikian marked this pull request as draft June 18, 2026 19:06
@gjtorikian gjtorikian added the autogenerated Autogenerated code or content label Jun 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autogenerated Autogenerated code or content

Development

Successfully merging this pull request may close these issues.

1 participant