Skip to content

feat!: Port DirectorySync to oagen#1626

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

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

Conversation

@gjtorikian

Copy link
Copy Markdown
Contributor

Description

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

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 })         │
└─────────────────┴─────────────────────┴─────────────────────────┘

As well:

  1. Timestamps are now Date objects, not strings. createdAt/updatedAt now come back as new Date(...).
  2. Directory.state values changed. The previous serializer mapped linked → active and unlinked → inactive. The new serializer passes the raw wire value
    straight through, so getDirectory/listDirectories now return state: 'linked' | 'unlinked' | 'validating' | 'invalid_credentials' | 'deleting'

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 category to the oagen code-generator, replacing hand-written implementations with auto-generated equivalents for four methods, updating timestamps from string to Date, and splitting monolithic interface files into per-type files.

  • Four methods (getDirectory, deleteDirectory, getGroup, getUser) changed from positional string arguments to options objects; all IDs are now encodeURIComponent-wrapped before being interpolated into URL paths.
  • createdAt/updatedAt across Directory, DirectoryGroup, and DirectoryUserWithGroups are now Date objects; DirectoryUser.state gains 'suspended'; new DirectoryMetadata sub-object added to Directory.
  • deserializeUpdatedEventDirectoryGroup moved to event-directory.serializer.ts; deserializeEventDirectory/deserializeDeletedEventDirectory extracted there as well, while listUsers/getUser remain hand-owned inside @oagen-ignore regions due to the custom-attributes generic.

Confidence Score: 4/5

Safe to merge with one correction: the PR description documents a breaking state-mapping change that the code does not actually make.

The serializer still maps 'linked' → 'active' and 'unlinked' → 'inactive', but the migration notes tell consumers the raw wire values now pass through. A consumer who follows the documented breaking change will swap 'active' checks for 'linked' checks and immediately break in production. Everything else — ID encoding, timestamp promotion, barrel exports, fixture-driven tests — is correct and consistent.

src/directory-sync/serializers/directory.serializer.ts — the state-mapping logic and the PR description need to be reconciled before this ships.

Important Files Changed

Filename Overview
src/directory-sync/directory-sync.ts Auto-generated; migrates four methods from positional string args to options objects and inlines serializeListDirectoriesOptions. Logic is correct; no new issues found.
src/directory-sync/serializers/directory.serializer.ts State mapping (linked→active, unlinked→inactive) retained in code but PR description claims it was removed — direct contradiction that will mislead migration.
src/directory-sync/serializers/event-directory.serializer.ts Receives moved functions from directory.serializer; EventDirectory timestamps remain strings while all other directory timestamps are now Date objects (pre-existing gap).
src/directory-sync/interfaces/directory.interface.ts Adds DirectoryMetadata, makes domain optional, moves DirectoryType/DirectoryState to separate files. DirectoryResponse.state still typed as DirectoryState (does not include 'linked'/'unlinked' wire values).
src/directory-sync/interfaces/index.ts GetUserOptions and all other new interfaces are now properly exported from the barrel.
src/directory-sync/directory-sync.spec.ts Tests replaced with fixture-driven oagen style; expectDirectory confirms linked→active mapping still works at runtime.
src/directory-sync/serializers/directory-group.serializer.ts Migrated to new oagen pattern; timestamps now converted to Date, object field defaulted.
src/directory-sync/interfaces/directory-state.interface.ts New file; defines DirectoryState as 'active'

Sequence Diagram

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

    Consumer->>DirectorySync: "getDirectory({ id })"
    DirectorySync->>WorkOS API: GET /directories/{encodeURIComponent(id)}
    WorkOS API-->>DirectorySync: DirectoryResponse (state: "linked")
    DirectorySync->>Serializer: deserializeDirectory(response)
    Serializer->>Serializer: deserializeDirectoryState("linked") → "active"
    Serializer-->>DirectorySync: Directory (state: "active", createdAt: Date, updatedAt: Date)
    DirectorySync-->>Consumer: Directory

    Consumer->>DirectorySync: "listDirectories({ organizationId, search })"
    DirectorySync->>DirectorySync: "serializeListDirectoriesOptions → { organization_id, search, ... }"
    DirectorySync->>WorkOS API: GET /directories?organization_id=...
    WorkOS API-->>DirectorySync: ListResponse[DirectoryResponse]
    DirectorySync->>Serializer: deserializeDirectory (per item)
    DirectorySync-->>Consumer: AutoPaginatable[Directory]
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
    participant Serializer

    Consumer->>DirectorySync: "getDirectory({ id })"
    DirectorySync->>WorkOS API: GET /directories/{encodeURIComponent(id)}
    WorkOS API-->>DirectorySync: DirectoryResponse (state: "linked")
    DirectorySync->>Serializer: deserializeDirectory(response)
    Serializer->>Serializer: deserializeDirectoryState("linked") → "active"
    Serializer-->>DirectorySync: Directory (state: "active", createdAt: Date, updatedAt: Date)
    DirectorySync-->>Consumer: Directory

    Consumer->>DirectorySync: "listDirectories({ organizationId, search })"
    DirectorySync->>DirectorySync: "serializeListDirectoriesOptions → { organization_id, search, ... }"
    DirectorySync->>WorkOS API: GET /directories?organization_id=...
    WorkOS API-->>DirectorySync: ListResponse[DirectoryResponse]
    DirectorySync->>Serializer: deserializeDirectory (per item)
    DirectorySync-->>Consumer: AutoPaginatable[Directory]
Loading

Reviews (6): Last reviewed commit: "refactor(directory-sync): generate the a..." | 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.

@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 on lines 35 to 41
export interface DirectoryResponse {
object: 'directory';
id: string;
domain: string;
external_key: string;
name: string;
organization_id?: string;
state: DirectoryStateResponse;
type: DirectoryType;
created_at: string;
updated_at: string;
}
interface EventDirectoryDomain {
object: 'organization_domain';
id: string;
domain: string;
}

export interface EventDirectory {
object: 'directory';
id: string;
externalKey: string;
type: DirectoryType;
state: DirectoryState;
name: string;
organizationId?: string;
domains: EventDirectoryDomain[];
createdAt: string;
updatedAt: string;
}

export interface EventDirectoryResponse {
object: 'directory';
id: string;
external_key: string;
type: DirectoryType;
state: DirectoryState;

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 DirectoryResponse.state is typed as DirectoryState (which is 'active' | 'inactive' | 'validating' | 'invalid_credentials' | 'deleting'), but the wire API actually sends 'linked' and 'unlinked' in place of 'active' and 'inactive'. This removes the old DirectoryStateResponse distinction, so TypeScript incorrectly promises that the raw HTTP response already contains the SDK-facing values. The two branches in deserializeDirectoryState that remap linkedactive and unlinkedinactive become unreachable from the type system's perspective (only avoided because the parameter is widened to string), making it easy to accidentally delete them in a future cleanup pass.

Suggested change
export interface DirectoryResponse {
object: 'directory';
id: string;
domain: string;
external_key: string;
name: string;
organization_id?: string;
state: DirectoryStateResponse;
type: DirectoryType;
created_at: string;
updated_at: string;
}
interface EventDirectoryDomain {
object: 'organization_domain';
id: string;
domain: string;
}
export interface EventDirectory {
object: 'directory';
id: string;
externalKey: string;
type: DirectoryType;
state: DirectoryState;
name: string;
organizationId?: string;
domains: EventDirectoryDomain[];
createdAt: string;
updatedAt: string;
}
export interface EventDirectoryResponse {
object: 'directory';
id: string;
external_key: string;
type: DirectoryType;
state: DirectoryState;
export type DirectoryStateResponse =
| 'linked'
| 'unlinked'
| 'validating'
| 'invalid_credentials'
| 'deleting';
export interface DirectoryResponse {
object: 'directory';
id: string;
organization_id?: string;
external_key: string;
type: DirectoryType;
state: DirectoryStateResponse;
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/directory-sync/interfaces/directory.interface.ts
Line: 35-41

Comment:
`DirectoryResponse.state` is typed as `DirectoryState` (which is `'active' | 'inactive' | 'validating' | 'invalid_credentials' | 'deleting'`), but the wire API actually sends `'linked'` and `'unlinked'` in place of `'active'` and `'inactive'`. This removes the old `DirectoryStateResponse` distinction, so TypeScript incorrectly promises that the raw HTTP response already contains the SDK-facing values. The two branches in `deserializeDirectoryState` that remap `linked``active` and `unlinked``inactive` become unreachable from the type system's perspective (only avoided because the parameter is widened to `string`), making it easy to accidentally delete them in a future cleanup pass.

```suggestion
export type DirectoryStateResponse =
  | 'linked'
  | 'unlinked'
  | 'validating'
  | 'invalid_credentials'
  | 'deleting';

export interface DirectoryResponse {
  object: 'directory';
  id: string;
  organization_id?: string;
  external_key: string;
  type: DirectoryType;
  state: DirectoryStateResponse;
```

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

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>
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