You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: rewrite SDK field names in OData filter/orderby/select/expand [PLT-102084] (#536)
* fix: rewrite SDK field names in OData filter/orderby/select/expand
Field maps applied SDK-side renames on responses only, so consumers had
to remember two names per field: the SDK name for reading the response
and the API name for filter/orderby/select/expand. Filtering by the SDK
name was silently forwarded to the API and rejected.
Add a token-aware rewriter that walks OData expressions, preserves
quoted string literals, and rewrites identifier tokens via the reversed
`{Entity}Map`. Wire it into `PaginationHelpers.getAll` and into the
direct `addPrefixToKeys` sites used by `getById` / `getByName` /
`processes.start` / bucket read-write URIs across Jobs, Queues, Assets,
Buckets, Processes, Attachments, and Tasks.
Refs: PLT-102084
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address convention review comments
- Replace `as any` mock-resolved values with typed factory helpers
(createMockTasks, createMockBuckets, createMockOrchestratorProcesses)
- Remove unused rewriteODataIdentifiers entry from transform mock —
only rewriteODataRequestFields is reached through service paths
- Type rewriteODataIdentifiers requestMap as FieldMapping for
consistency with the rest of transform.ts
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor: narrow PR scope to Jobs; rename helper to transformOptions
Per discussion, scoping this PR to a single service (Jobs) so reviewers
can evaluate the approach in isolation. Other affected services
(Queues, Assets, Buckets, Processes, Attachments, Tasks) will be picked
up in follow-up PRs that reuse the same plumbing introduced here.
Also renames the public helper rewriteODataRequestFields → transformOptions
to mirror the existing transformRequest naming. transformRequest is for
request bodies (object keys), transformOptions is for OData query options
(strings with embedded identifiers); same reversed-map mechanism under
the hood.
Behavior change: none beyond what's already shipping for Jobs.
* refactor: simplify rewriteODataIdentifiers with regex alternation
Replace the ~30-line char-by-char state machine with a 4-line
String.prototype.replace using a regex that alternates between
single-quoted literals (with '' escape) and OData identifiers.
The regex engine handles both states for free via alternation and
greedy identifier matching:
'(?:[^']|'')*' atomic quoted literal (identifiers inside don't
get a separate match)
[A-Za-z_][A-Za-z0-9_]* full identifier token (no substring collisions)
All 17 transform.test.ts cases pass unchanged — same invariants
(literal-atomicity, full-token matching, empty-map no-op).
* refactor: call transformOptions in service layer for symmetry with transformRequest
Removes the fieldMap config option from PaginationHelpers / GetAllConfig
and moves the transformOptions call into JobService.getAll, mirroring
the existing pattern where services call transformRequest directly on
request bodies before delegating to the HTTP/pagination layer.
Benefits:
- PaginationHelpers stays generic — no service-specific concept like
"field map" in the shared helper.
- One convention across the SDK: services own request-side translation,
whether the data is a body (transformRequest) or OData query options
(transformOptions). The helpers layer plumbs only.
- Unit test asserts the post-translation shape directly (filter contains
releaseName, not processName) rather than asserting a wiring detail.
No behavior change.
* fix(tests): address claude bot review comments
- Rename misleading operator-collision test: the assertion proves the
rewriter DOES rewrite operator-named map keys, so the test name must
not say "does not rewrite operators".
- Drop the `null as unknown as string` cast — `transformOptions`'s
generic accepts the wider type directly.
* fix: drop redundant `as T` cast on transformOptions return
transformOptions's generic signature `<T extends Record<string, any>>(options: T, ...): T`
already returns T; the explicit cast was redundant.
* fix(jobs): address PR review feedback
- Consolidate JSDoc filter example: drop the unrelated `state eq 'Running'`
case and the verbose "SDK field names" preamble; keep a single example
demonstrating filter + orderby with the renamed processName and createdTime.
- Simplify getById's option-prep into a single `apiFieldOptions` chain:
transformOptions → addPrefixToKeys in two lines, no intermediate
`rewrittenOptions` / `keysToPrefix` variables.
- Add an integration test that exercises SDK-renamed fields end-to-end:
`orderby: 'createdTime desc'` and `select: 'key,processName,state,createdTime'`.
The Orchestrator API would 400 on either of those without the rewriter,
so this test fails if the rewrite pipeline regresses.
* refactor: remove chained renames from transformData
The previous implementation iterated the fieldMapping entries and mutated
`result` in place — which meant a map with both `a → b` and `b → c` would
chain through both renames whenever the input had `a`, producing `c`
instead of `b`. ProcessMap's `releaseKey → processKey` + `processKey →
packageKey` entries are the canonical example; the chain was a latent
quirk waiting to misbehave on a response that contained both keys.
The new implementation walks `Object.entries(data)` and looks each key up
in the mapping exactly once. Renames are independent — `a → b` does not
compose with `b → c` because the result is built fresh and never re-read
inside the loop. The original key is dropped (no source + target
coexistence in the result).
Side effects:
- Strictly cheaper: O(D) iterations with no upfront shallow copy and no
`delete` calls. The previous code was O(D + M) plus V8 hidden-class
transitions on every delete.
- Output key order now matches the input's iteration order, which is
slightly more predictable.
- All 1898 pre-existing unit tests still pass — no test mock contained
both ends of a ProcessMap chain, so the chained behaviour was never
actually exercised. The removal is observably a no-op for existing
inputs.
7 new tests pin the no-chain contract:
- basic rename
- passthrough of unmapped keys
- original key dropped (no source + target coexistence)
- no chaining (`a → b → c` map produces `b`, not `c`)
- both ends of a chain present in input produce independent renames
- array recursion
- empty input
* feat(processes): expose processKey on ProcessStartResponse
The Orchestrator start endpoint returns the originating process's
ReleaseKey alongside the job's own Key. With the no-chaining
transformData, ProcessMap's `releaseKey → processKey` entry now
delivers that as `processKey` on the response without accidentally
re-renaming it to `packageKey` (which the old chained transform did).
Adding the typed field surfaces it on the public response. No
behaviour change at runtime — the field was already being populated
by the rename pipeline; it just wasn't declared on the type.
* docs(processes): trim processKey JSDoc
* fix(transform): handle null/undefined input in transformData
The previous chained implementation tolerated null/undefined via the
shallow spread (`{ ...null }` is `{}`). The refactored implementation
called `Object.entries(data)` which throws `TypeError: Cannot convert
undefined or null to object`.
The CI integration test `attachments.integration.test.ts` exposed this:
`AttachmentService.getById` calls `transformData(camelCased.blobFileAccess,
BucketMap)`, and when an OData `select` excludes `blobFileAccess`, the
field arrives undefined. The old code returned `{}` silently; the new
code threw.
Restore null/undefined tolerance with an early return that passes the
value through unchanged. Add two unit tests pinning the behaviour.
* refactor(transform): tighten transformOptions per review comments
- Drop the `any` from the generic constraint. Switch to
`T extends object` (matching `transformRequest`'s pattern) and cast
the spread destination to `Record<string, unknown>` so we keep one
named cast instead of leaking `any` through the signature and the
internal result type.
- Remove the `!options || !responseMap` runtime guard. Both parameters
are non-optional in the type contract, and the two current call sites
in jobs.ts already guard `options?` before invoking. Per
conventions.md: don't add validation for scenarios that can't happen.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0 commit comments