Skip to content

feat!: Port DirectorySync to oagen#1626

Draft
gjtorikian wants to merge 1 commit into
mainfrom
oagen/own-directory-sync
Draft

feat!: Port DirectorySync to oagen#1626
gjtorikian wants to merge 1 commit 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 module to the oagen code generator, migrating four methods from positional string arguments to options objects and replacing the hand-written serializers with generated ones. Two significant behavioral changes land alongside the refactor: createdAt/updatedAt fields across Directory, DirectoryGroup, and DirectoryUserWithGroups are now Date objects, and directory state values now pass through raw wire values ('linked', 'unlinked', …) instead of mapping to the previous 'active'/'inactive' aliases.

  • API surface: getDirectory, deleteDirectory, getGroup, and getUser now take { id } options objects; listGroups/listUsers signatures are unchanged.
  • Type additions: DirectoryMetadata, DirectoryState, DirectoryType, SlimRole, and related interfaces are now dedicated files generated from the OpenAPI spec.
  • Three issues found: GetUserOptions is not re-exported from the public barrel; organizationId is coerced to '' when absent rather than preserving undefined; and the generated DirectoryUserWithGroupsState constant exposes a 'suspended' value that the hand-maintained DirectoryUser.state type does not include.

Confidence Score: 3/5

The core logic is correct but three defects in the generated/hand-maintained layer should be resolved before shipping.

The organizationId ?? '' coercion changes a field that was previously optional to always-present-but-possibly-empty, which is a silent contract break for downstream consumers. GetUserOptions being absent from the barrel means the type is effectively undiscoverable to SDK users. The DirectoryUserWithGroupsState/DirectoryUser.state mismatch means 'suspended' users would be unhandled at the type level. All three touch the public API surface of a major release.

src/directory-sync/serializers/directory.serializer.ts (organizationId coercion), src/directory-sync/interfaces/index.ts (missing GetUserOptions export), src/directory-sync/interfaces/directory-user-with-groups-state.interface.ts (suspended state inconsistency)

Important Files Changed

Filename Overview
src/directory-sync/directory-sync.ts Core service class ported to oagen: methods accept options objects instead of raw strings, getGroup is now generated (with encodeURIComponent), and a local serializeListDirectoriesOptions replaces the old shared serializer.
src/directory-sync/serializers/directory.serializer.ts Simplified serializer with Date conversion; organizationId incorrectly coerced to empty string instead of preserving undefined when absent.
src/directory-sync/interfaces/index.ts Barrel updated for new interfaces but get-user-options.interface is missing from exports, making GetUserOptions unavailable to public consumers.
src/directory-sync/interfaces/directory-user-with-groups-state.interface.ts New generated state constant includes 'suspended' which is absent from the hand-maintained DirectoryUser.state type union — inconsistency between generated and hand-owned interfaces.
src/directory-sync/serializers/event-directory.serializer.ts Moved out of directory.serializer; timestamps remain raw strings while all other entity serializers now produce Date objects — inconsistency across the module.
src/directory-sync/interfaces/directory-group.interface.ts Timestamps migrated to Date, rawAttributes made optional; clean, consistent with the rest of the PR.
src/directory-sync/interfaces/directory.interface.ts DirectoryType, DirectoryState, and metadata split into dedicated interfaces; state values now pass raw wire values instead of mapping linked→active/unlinked→inactive; domain made optional.
src/directory-sync/directory-sync.spec.ts Tests rewritten to use fixture files and helper functions; covers list/get/delete paths including new Date-based timestamp assertions.

Sequence Diagram

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

    Consumer->>DirectorySync: "getDirectory({ id })"
    DirectorySync->>WorkOSClient: GET /directories/:id
    WorkOSClient-->>DirectorySync: DirectoryResponse (snake_case, string timestamps)
    DirectorySync->>Serializer: deserializeDirectory(response)
    Serializer-->>DirectorySync: Directory (camelCase, Date timestamps, raw state)
    DirectorySync-->>Consumer: Directory

    Consumer->>DirectorySync: "listDirectories({ organizationId, search })"
    DirectorySync->>DirectorySync: serializeListDirectoriesOptions()
    DirectorySync->>WorkOSClient: "GET /directories?organization_id=..."
    WorkOSClient-->>DirectorySync: "ListResponse<DirectoryResponse>"
    DirectorySync->>Serializer: deserializeDirectory (per item)
    DirectorySync-->>Consumer: "AutoPaginatable<Directory>"

    Consumer->>DirectorySync: "deleteDirectory({ id })"
    DirectorySync->>WorkOSClient: DELETE /directories/:id
    WorkOSClient-->>DirectorySync: 204
    DirectorySync-->>Consumer: void
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 WorkOSClient
    participant Serializer

    Consumer->>DirectorySync: "getDirectory({ id })"
    DirectorySync->>WorkOSClient: GET /directories/:id
    WorkOSClient-->>DirectorySync: DirectoryResponse (snake_case, string timestamps)
    DirectorySync->>Serializer: deserializeDirectory(response)
    Serializer-->>DirectorySync: Directory (camelCase, Date timestamps, raw state)
    DirectorySync-->>Consumer: Directory

    Consumer->>DirectorySync: "listDirectories({ organizationId, search })"
    DirectorySync->>DirectorySync: serializeListDirectoriesOptions()
    DirectorySync->>WorkOSClient: "GET /directories?organization_id=..."
    WorkOSClient-->>DirectorySync: "ListResponse<DirectoryResponse>"
    DirectorySync->>Serializer: deserializeDirectory (per item)
    DirectorySync-->>Consumer: "AutoPaginatable<Directory>"

    Consumer->>DirectorySync: "deleteDirectory({ id })"
    DirectorySync->>WorkOSClient: DELETE /directories/:id
    WorkOSClient-->>DirectorySync: 204
    DirectorySync-->>Consumer: void
Loading

Reviews (1): Last reviewed commit: "Port DirectorySync to oagen" | Re-trigger Greptile

Comment on lines +15 to +19
export * from './get-directory-options.interface';
export * from './list-directories-options.interface';
export * from './list-groups-options.interface';
export * from './list-directory-users-options.interface';
export * from './directory-user.interface';
export * from './list-groups-options.interface';
export * from './slim-role.interface';

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 GetUserOptions missing from public barrel export

GetUserOptions (used as the parameter type for getUser) is not re-exported from the barrel. Every other new options interface introduced in this PR — DeleteDirectoryOptions, DirectorySyncGetGroupOptions, GetDirectoryOptions — is exported, but GetUserOptions is absent. TypeScript consumers who want to explicitly annotate variables or write helper functions around getUser will be unable to import the type from the package's public interface index.

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!

updatedAt: directory.updated_at,
object: response.object,
id: response.id,
organizationId: response.organization_id ?? '',

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 organizationId coerced to empty string when absent

Directory.organizationId is declared organizationId?: string (optional), so the correct mapping when organization_id is absent is to omit the field or assign undefined. Using ?? '' silently sets it to an empty string instead, which violates the declared type contract. Any consumer checking directory.organizationId === undefined (or using the field as an optional discriminator) will get incorrect behaviour — the field will always be present and truthy except for the empty-string edge case, which is harder to reason about.

Suggested change
organizationId: response.organization_id ?? '',
organizationId: response.organization_id ?? undefined,

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