feat!: Port DirectorySync to oagen#1626
Conversation
Greptile SummaryThis PR ports the
Confidence Score: 4/5Safe 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
|
| // 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]; |
There was a problem hiding this comment.
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.
| domains: directory.domains, | ||
| createdAt: directory.created_at, | ||
| updatedAt: directory.updated_at, |
There was a problem hiding this comment.
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.
| organization_id?: string; | ||
| domains: EventDirectoryDomain[]; | ||
| domain?: string; | ||
| metadata?: DirectoryMetadataResponse; |
There was a problem hiding this 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.
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.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>
| 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; |
There was a problem hiding this 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.
| 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>
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:
As well:
createdAt/updatedAtnow come back asnew Date(...).Directory.statevalues changed. The previous serializer mappedlinked → activeandunlinked → inactive. The new serializer passes the raw wire valuestraight through, so
getDirectory/listDirectoriesnow return state:'linked' | 'unlinked' | 'validating' | 'invalid_credentials' | 'deleting'Because of these changes, this is a major breaking change to the SDK.