Skip to content

feat!: Port DirectorySync to oagen#1626

Open
gjtorikian wants to merge 6 commits into
mainfrom
oagen/own-directory-sync
Open

feat!: Port DirectorySync to oagen#1626
gjtorikian wants to merge 6 commits into
mainfrom
oagen/own-directory-sync

Conversation

@gjtorikian

@gjtorikian gjtorikian commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Description

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

Crucially, the serializer still maps the wire values linked → active and unlinked → inactive, preserving a difference between the backend and the SDK.

Breaking changes

1. Four methods switched from a positional string argument to an options object:

┌─────────────────┬─────────────────────┬─────────────────────────┐
│     Method      │       Before        │          After          │
├─────────────────┼─────────────────────┼─────────────────────────┤
│ getDirectory    │ getDirectory(id)    │ getDirectory({ id })    │
├─────────────────┼─────────────────────┼─────────────────────────┤
│ deleteDirectory │ deleteDirectory(id) │ deleteDirectory({ id }) │
├─────────────────┼─────────────────────┼─────────────────────────┤
│ getGroup        │ getGroup(group)     │ getGroup({ id })        │
├─────────────────┼─────────────────────┼─────────────────────────┤
│ getUser         │ getUser(user)       │ getUser({ id })         │
└─────────────────┴─────────────────────┴─────────────────────────┘

2. Timestamps are now Date objects, not strings. createdAt/updatedAt on Directory, DirectoryGroup, and DirectoryUserWithGroups now come back as new Date(...).

3. Directory.domain is now optional. It changed from domain: string to domain?: string, so it is now typed string | undefined. Consumers that assign directory.domain to a string-typed variable will see a TypeScript error.

Because of these changes, this is a major breaking change to the SDK.

@gjtorikian gjtorikian requested review from a team as code owners June 16, 2026 23:55
@gjtorikian gjtorikian requested a review from stanleyphu June 16, 2026 23:55
@greptile-apps

greptile-apps Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR ports the DirectorySync module to be generated from the OpenAPI spec via oagen, refactoring interfaces, serializers, and the main class. It introduces @oagen-ignore blocks to preserve hand-owned generics for listUsers/getUser, and correctly maintains the linked→active / unlinked→inactive state remapping through the new private deserializeDirectoryState.

  • Breaking API changes: four methods (getDirectory, deleteDirectory, getGroup, getUser) now take options objects; createdAt/updatedAt fields on Directory, DirectoryGroup, and DirectoryUserWithGroups are now Date objects; Directory.domain is now optional.
  • New types/interfaces: DirectoryState, DirectoryType, DirectoryMetadata, SlimRole, EventDirectory, and several options interfaces are split into dedicated files and re-exported from the barrel.
  • Event serializers consolidated: deserializeEventDirectory, deserializeDeletedEventDirectory, and deserializeUpdatedEventDirectoryGroup are moved into a new event-directory.serializer.ts and re-exported from serializers/index.ts, keeping event.serializer.ts imports intact.

Confidence Score: 5/5

Safe to merge; all serializer logic, state remapping, and URL encoding are correct, and the events/webhooks import chain is intact after moving the event-directory serializers.

The core serialization logic — linked-to-active/unlinked-to-inactive remapping, Date conversion, encodeURIComponent on path segments, and the module-private serializeListDirectoriesOptions — is all correct. The @oagen-ignore boundary preserves the hand-owned generic listUsers/getUser through future regenerations. The two findings are documentation/coverage gaps rather than runtime defects.

src/directory-sync/interfaces/directory-group.interface.ts (undocumented rawAttributes breaking change) and src/directory-sync/directory-sync.spec.ts (no coverage for listUsers/getUser).

Important Files Changed

Filename Overview
src/directory-sync/directory-sync.ts Auto-generated class refactored to use options objects and encodeURIComponent on all path parameters; serializeListDirectoriesOptions correctly inlined as module-private.
src/directory-sync/serializers/directory.serializer.ts deserializeDirectory now returns Date objects and passes organization_id directly (no ?? '' coercion); deserializeDirectoryState made private as expected.
src/directory-sync/serializers/event-directory.serializer.ts New file consolidating EventDirectory serializers; EventDirectory.createdAt/updatedAt remain strings (per EventDirectory interface), consistent with intentional split from Directory.
src/directory-sync/interfaces/directory-group.interface.ts rawAttributes changed from required any to optional Record<string, any> — an undocumented breaking change for TypeScript consumers; createdAt/updatedAt now Date.
src/directory-sync/directory-sync.spec.ts Auto-generated spec replaces comprehensive hand-authored tests; listUsers and getUser (hand-owned methods) no longer have any test coverage.
src/directory-sync/interfaces/index.ts Barrel re-exports all new interfaces including GetUserOptions; DirectoryUser/DirectoryUserResponse also selectively re-exported via export type to avoid conflicts.
src/directory-sync/serializers/directory-group.serializer.ts deserializeDirectoryGroup now converts timestamps to Date and includes the object field; deserializeUpdatedEventDirectoryGroup correctly moved to event-directory.serializer.ts.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Consumer
    participant DirectorySync
    participant WorkOS_API as WorkOS API
    participant Serializer

    Note over DirectorySync: Auto-generated by oagen
    Consumer->>DirectorySync: "listDirectories({ organizationId, search })"
    DirectorySync->>DirectorySync: serializeListDirectoriesOptions()
    DirectorySync->>WorkOS_API: "GET /directories?organization_id=..."
    WorkOS_API-->>DirectorySync: DirectoryResponse[]
    DirectorySync->>Serializer: deserializeDirectory(response)
    Serializer->>Serializer: linked to active remapping
    Serializer->>Serializer: new Date(created_at)
    Serializer-->>DirectorySync: Directory
    DirectorySync-->>Consumer: AutoPaginatable of Directory

    Consumer->>DirectorySync: "getGroup({ id })"
    DirectorySync->>WorkOS_API: GET /directory_groups/encoded_id
    WorkOS_API-->>DirectorySync: DirectoryGroupResponse
    DirectorySync->>Serializer: deserializeDirectoryGroup(response)
    Serializer-->>DirectorySync: DirectoryGroup (createdAt: Date)
    DirectorySync-->>Consumer: DirectoryGroup

    Note over Consumer,Serializer: listUsers/getUser are hand-owned
    Consumer->>DirectorySync: "getUser({ id })"
    DirectorySync->>WorkOS_API: GET /directory_users/encoded_id
    WorkOS_API-->>DirectorySync: DirectoryUserWithGroupsResponse
    DirectorySync->>Serializer: deserializeDirectoryUserWithGroups(response)
    Serializer-->>DirectorySync: DirectoryUserWithGroups
    DirectorySync-->>Consumer: DirectoryUserWithGroups
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 Consumer
    participant DirectorySync
    participant WorkOS_API as WorkOS API
    participant Serializer

    Note over DirectorySync: Auto-generated by oagen
    Consumer->>DirectorySync: "listDirectories({ organizationId, search })"
    DirectorySync->>DirectorySync: serializeListDirectoriesOptions()
    DirectorySync->>WorkOS_API: "GET /directories?organization_id=..."
    WorkOS_API-->>DirectorySync: DirectoryResponse[]
    DirectorySync->>Serializer: deserializeDirectory(response)
    Serializer->>Serializer: linked to active remapping
    Serializer->>Serializer: new Date(created_at)
    Serializer-->>DirectorySync: Directory
    DirectorySync-->>Consumer: AutoPaginatable of Directory

    Consumer->>DirectorySync: "getGroup({ id })"
    DirectorySync->>WorkOS_API: GET /directory_groups/encoded_id
    WorkOS_API-->>DirectorySync: DirectoryGroupResponse
    DirectorySync->>Serializer: deserializeDirectoryGroup(response)
    Serializer-->>DirectorySync: DirectoryGroup (createdAt: Date)
    DirectorySync-->>Consumer: DirectoryGroup

    Note over Consumer,Serializer: listUsers/getUser are hand-owned
    Consumer->>DirectorySync: "getUser({ id })"
    DirectorySync->>WorkOS_API: GET /directory_users/encoded_id
    WorkOS_API-->>DirectorySync: DirectoryUserWithGroupsResponse
    DirectorySync->>Serializer: deserializeDirectoryUserWithGroups(response)
    Serializer-->>DirectorySync: DirectoryUserWithGroups
    DirectorySync-->>Consumer: DirectoryUserWithGroups
Loading

Reviews (9): Last reviewed commit: "refactor(directory-sync): regenerate wit..." | Re-trigger Greptile

Comment thread src/directory-sync/interfaces/index.ts
Comment thread src/directory-sync/serializers/directory.serializer.ts Outdated
Comment on lines +1 to +10
// This file is auto-generated by oagen. Do not edit.

export const DirectoryUserWithGroupsState = {
Active: 'active',
Suspended: 'suspended',
Inactive: 'inactive',
} as const;

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

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.

P1 DirectoryUserWithGroupsState includes 'suspended' but DirectoryUser.state does not

The auto-generated DirectoryUserWithGroupsState constant exports three values — Active, Suspended, and Inactive — implying the wire API can return 'suspended'. However, the hand-maintained DirectoryUser.state type in directory-user.interface.ts is still typed as 'active' | 'inactive'. A directory user whose state is 'suspended' in the provider will carry the value at runtime, but TypeScript consumers trusting the declared union will never expect or handle it. Either the state field in DirectoryUser should include 'suspended', or the generated constant should be reconciled with the hand-owned interface so they don't conflict.

Comment on lines +19 to +21
domains: directory.domains,
createdAt: directory.created_at,
updatedAt: directory.updated_at,

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 EventDirectory timestamps remain string while all other timestamp fields are now Date

Directory, DirectoryGroup, and DirectoryUserWithGroups all had their createdAt/updatedAt migrated to Date objects in this PR. EventDirectory still assigns raw strings (directory.created_at, directory.updated_at) with no new Date() conversion, and the EventDirectory interface retains createdAt: string. Consumers who process both event and non-event directory objects will now encounter an inconsistent type for the same conceptual field.

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.

Is this intended?

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.

This does appear to be an oversight rather than intentional. The EventDirectory interface has createdAt: string / updatedAt: string while Directory (the non-event counterpart) uses Date objects — and both represent the same conceptual fields from the same wire format.

If it is intentional (e.g., EventDirectory is deliberately kept as raw strings because it's embedded in webhook payloads and you want to avoid the conversion overhead or a type mismatch with other event types), it's worth calling that out explicitly. Otherwise, the fix would be straightforward:

Suggested change
domains: directory.domains,
createdAt: directory.created_at,
updatedAt: directory.updated_at,
createdAt: new Date(directory.created_at),
updatedAt: new Date(directory.updated_at),

And update event-directory.interface.ts to match:

  createdAt: Date;
  updatedAt: Date;

@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
@gjtorikian gjtorikian marked this pull request as ready for review June 23, 2026 18:10
organization_id?: string;
domains: EventDirectoryDomain[];
domain?: string;
metadata?: DirectoryMetadataResponse;

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.

P1 domain silently changed from required to optional — undocumented breaking change

The PR description calls out timestamp type changes and state value changes as breaking, but domain quietly moved from domain: string (required) to domain?: string. Any consumer code that destructures or assigns directory.domain to a string-typed variable will now produce a TypeScript error (Type 'string | undefined' is not assignable to type 'string'), and code that passes a Directory to a function expecting { domain: string } will break at the call site. Since this is already a major-bump PR, it needs to be listed explicitly in the migration guide alongside the other breaking changes.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/directory-sync/interfaces/directory.interface.ts
Line: 44

Comment:
**`domain` silently changed from required to optional — undocumented breaking change**

The PR description calls out timestamp type changes and `state` value changes as breaking, but `domain` quietly moved from `domain: string` (required) to `domain?: string`. Any consumer code that destructures or assigns `directory.domain` to a `string`-typed variable will now produce a TypeScript error (`Type 'string | undefined' is not assignable to type 'string'`), and code that passes a `Directory` to a function expecting `{ domain: string }` will break at the call site. Since this is already a major-bump PR, it needs to be listed explicitly in the migration guide alongside the other breaking changes.

How can I resolve this? If you propose a fix, please make it concise.

gjtorikian and others added 3 commits June 26, 2026 14:01
The Directory Sync API can return a `suspended` state for
directory users (e.g. SCIM-deactivated accounts), but the SDK
types only modeled `active` and `inactive`. Consumers had no
way to type-check against the suspended case.
Apply the deterministic output of the oagen-emitters fixes (node-bugfixes)
directly, so this PR is correct ahead of a full re-generation:

- Export `GetUserOptions` from the interfaces barrel. It was emitted and
  imported by the hand-owned `getUser`, but a cross-service collision with
  UserManagement's same-named `GetUserOptions` kept it out of the barrel.
- Stop coercing `organizationId` to `''` when `organization_id` is absent;
  the field is optional, so pass the wire value (`undefined`) through.
- Remove the orphaned `list-directories-options.serializer.ts`; its logic is
  inlined in `directory-sync.ts` and the file is referenced nowhere.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The oagen port exposed raw wire states (linked/unlinked/...) on Directory.state,
a breaking change for consumers relying on the SDK's historical active/inactive
values. The spec only describes the raw enum, so the linked->active /
unlinked->inactive translation is restored as hand-owned:

- directory-state.interface.ts: DirectoryState back to the mapped union;
  @oagen-ignore-file + dropped from the manifest.
- directory.serializer.ts: restore deserializeDirectoryState and apply it;
  @oagen-ignore-file + dropped from the manifest.
- directory-sync.spec.ts: fixture wire state `linked` now deserializes to `active`.

EventDirectory.state already referenced DirectoryState, so it tracks the mapped
union as on main (event payloads still pass through unmapped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/directory-sync/interfaces/directory.interface.ts Outdated
Replace the hand-owned `@oagen-ignore-file` carve-out (58c2a08) with generated
code now that the emitter supports enum value remaps (oagen-emitters
`enumValueRemaps` + the openapi-spec config). `DirectoryState` and
`directory.serializer.ts`'s `deserializeDirectoryState` mapper are regenerated
(autogen headers restored, back in the manifest); behavior is unchanged
(`linked`→`active`, `unlinked`→`inactive`) but a clean regen now reproduces it
instead of skipping the files. tsc clean, directory-sync tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gjtorikian gjtorikian added the breaking change Contains a breaking change label Jun 30, 2026

@qbalin qbalin left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@gjtorikian Happy to approve. Are the greptile comments worth addressing in this PR?

… type

Regenerate DirectorySync with the enum wire-companion emitter change: DirectoryResponse.state is now typed DirectoryStateResponse (raw wire linked/unlinked) and deserializeDirectoryState maps it to the DirectoryState domain type (active/inactive), restoring the wire/domain distinction flagged in review. Also picks up current-emitter regeneration of the directory-sync tests and serializers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gjtorikian

Copy link
Copy Markdown
Contributor Author

@qbalin Thanks!

I think all of this Greptile feedback is already addressed, it's just not clearing (no matter how many times I hit retrigger). This one, for instance, will be documented in the changelog and matches what the spec provides; this one also matches the existing webhook string timestamps and is intentional.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autogenerated Autogenerated code or content breaking change Contains a breaking change

Development

Successfully merging this pull request may close these issues.

3 participants