From 1974f73bf104212624e7379a8cc0df529c520139 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 10 Jun 2026 17:46:02 +0200 Subject: [PATCH 1/4] refactor(agent-client): move polymorphic linkage read into the lib [PRD-493] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workflow-executor adapter hand-rolled a fetch (URL building, token, query params) to read a polymorphic relation's raw JSON:API linkage. That's exactly what agent-client exists to centralise. Move it into the lib: - HttpRequester.query gains a `raw` option to return the undeserialized body (the deserializer drops relationship `type`, the polymorphic discriminator). - Collection.getRelationLinkage(id, relation) reads the linkage { type, id } via a `@@@id` projection, reusing QuerySerializer + auth. - AgentClientAgentPort.resolvePolymorphicType now just delegates to it — no more hand-rolled fetch/URL/token. Tests updated on both sides (agent-client + executor adapter). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../agent-client/src/domains/collection.ts | 21 +++++++++ packages/agent-client/src/http-requester.ts | 8 ++++ .../test/domains/collection.test.ts | 35 +++++++++++++++ .../src/adapters/agent-client-agent-port.ts | 32 +++----------- .../adapters/agent-client-agent-port.test.ts | 43 ++++++------------- 5 files changed, 82 insertions(+), 57 deletions(-) diff --git a/packages/agent-client/src/domains/collection.ts b/packages/agent-client/src/domains/collection.ts index 7438caafb0..cbc0439ec9 100644 --- a/packages/agent-client/src/domains/collection.ts +++ b/packages/agent-client/src/domains/collection.ts @@ -179,6 +179,27 @@ export default class Collection extends CollectionChart { }); } + // Reads a to-one relation's raw JSON:API linkage ({ type, id }) via a `@@@id` projection. + // Used for polymorphic relations where the target `type` is the discriminator: the deserializer + // drops relationship types, so the raw body is read. Returns null when there is no linked record. + async getRelationLinkage( + id: RecordId, + relation: string, + ): Promise<{ type: string; id: string } | null> { + const body = await this.httpRequester.query<{ + data?: { relationships?: Record }; + }>({ + method: 'get', + path: `/forest/${this.name}/${serializeRecordId(id)}`, + query: QuerySerializer.serialize({ fields: [`${relation}@@@id`] }, this.name), + raw: true, + }); + + const linkage = body?.data?.relationships?.[relation]?.data; + + return linkage?.type ? { type: String(linkage.type), id: String(linkage.id) } : null; + } + private getActionInfo( actionEndpoints: ActionEndpointsByCollection, collectionName: string, diff --git a/packages/agent-client/src/http-requester.ts b/packages/agent-client/src/http-requester.ts index 9e70ad2287..5fdadd85a3 100644 --- a/packages/agent-client/src/http-requester.ts +++ b/packages/agent-client/src/http-requester.ts @@ -26,6 +26,7 @@ export default class HttpRequester { query, maxTimeAllowed, contentType, + raw, }: { method: 'get' | 'post' | 'put' | 'delete'; path: string; @@ -33,6 +34,9 @@ export default class HttpRequester { query?: Record; maxTimeAllowed?: number; contentType?: 'application/json' | 'text/csv'; + // Return the raw JSON:API body instead of deserializing. The deserializer drops relationship + // `type`, so callers needing the polymorphic discriminator read the raw linkage themselves. + raw?: boolean; }): Promise { try { const url = this.buildUrl(path); @@ -48,6 +52,10 @@ export default class HttpRequester { const response = await req; + if (raw) { + return response.body as Data; + } + try { return (await this.deserializer.deserialize(response.body)) as Data; } catch { diff --git a/packages/agent-client/test/domains/collection.test.ts b/packages/agent-client/test/domains/collection.test.ts index bae15fe19e..9241ce8e5e 100644 --- a/packages/agent-client/test/domains/collection.test.ts +++ b/packages/agent-client/test/domains/collection.test.ts @@ -174,6 +174,41 @@ describe('Collection', () => { }); }); + describe('getRelationLinkage', () => { + it('reads the raw linkage via a @@@id projection and returns { type, id }', async () => { + httpRequester.query.mockResolvedValue({ + data: { relationships: { commentable: { data: { type: 'orders', id: '99' } } } }, + }); + + const result = await collection.getRelationLinkage(7, 'commentable'); + + expect(httpRequester.query).toHaveBeenCalledWith( + expect.objectContaining({ method: 'get', path: '/forest/users/7', raw: true }), + ); + expect(result).toEqual({ type: 'orders', id: '99' }); + }); + + it('pipe-encodes composite ids', async () => { + httpRequester.query.mockResolvedValue({ + data: { relationships: { commentable: { data: { type: 'orders', id: '1|2' } } } }, + }); + + await collection.getRelationLinkage([1, 'abc'], 'commentable'); + + expect(httpRequester.query).toHaveBeenCalledWith( + expect.objectContaining({ path: '/forest/users/1|abc', raw: true }), + ); + }); + + it('returns null when there is no linked record', async () => { + httpRequester.query.mockResolvedValue({ + data: { relationships: { commentable: { data: null } } }, + }); + + expect(await collection.getRelationLinkage(7, 'commentable')).toBeNull(); + }); + }); + describe('delete', () => { it('should call httpRequester.query with DELETE method and correct body', async () => { httpRequester.query.mockResolvedValue({}); diff --git a/packages/workflow-executor/src/adapters/agent-client-agent-port.ts b/packages/workflow-executor/src/adapters/agent-client-agent-port.ts index 8046319029..9e3038c333 100644 --- a/packages/workflow-executor/src/adapters/agent-client-agent-port.ts +++ b/packages/workflow-executor/src/adapters/agent-client-agent-port.ts @@ -247,37 +247,15 @@ export default class AgentClientAgentPort implements AgentPort { }); } - // The agent-client deserializer drops relationship `type`, so read the raw record-with-projection - // response: `data.relationships..data = { type, id }`. No UI-exposed discriminator needed. + // Reads the polymorphic relation's target via agent-client's raw linkage read (the deserializer + // drops relationship `type`). agent-client owns URL/auth/projection — no hand-rolled fetch here. async resolvePolymorphicType( { collection, id, relation }: ResolvePolymorphicTypeQuery, user: StepUser, ): Promise<{ type: string; id: string } | null> { - return this.callAgent('resolvePolymorphicType', async () => { - const recordId = id.map(String).join('|'); - const params = new URLSearchParams({ - [`fields[${collection}]`]: relation, - [`fields[${relation}]`]: 'id', - timezone: 'Europe/Paris', // matches HttpRequester's default - }); - const base = this.agentUrl.replace(/\/+$/, ''); - const response = await fetch(`${base}/forest/${collection}/${recordId}?${params}`, { - headers: { Authorization: `Bearer ${this.mintToken(user)}`, Accept: 'application/json' }, - }); - - if (!response.ok) { - throw new Error( - `resolvePolymorphicType ${collection}/${recordId}: HTTP ${response.status}`, - ); - } - - const body = (await response.json()) as { - data?: { relationships?: Record }; - }; - const linkage = body?.data?.relationships?.[relation]?.data; - - return linkage?.type ? { type: String(linkage.type), id: String(linkage.id) } : null; - }); + return this.callAgent('resolvePolymorphicType', async () => + this.createClient(user).collection(collection).getRelationLinkage(id, relation), + ); } // Hits GET /forest/ (public, no auth required across all agent versions). A 4xx here means diff --git a/packages/workflow-executor/test/adapters/agent-client-agent-port.test.ts b/packages/workflow-executor/test/adapters/agent-client-agent-port.test.ts index 6f8892f1c6..e3a6de5e63 100644 --- a/packages/workflow-executor/test/adapters/agent-client-agent-port.test.ts +++ b/packages/workflow-executor/test/adapters/agent-client-agent-port.test.ts @@ -26,6 +26,7 @@ function createMockClient() { list: jest.fn(), getOne: jest.fn(), update: jest.fn(), + getRelationLinkage: jest.fn(), relation: jest.fn().mockReturnValue(mockRelation), action: jest.fn().mockResolvedValue(mockAction), }; @@ -824,24 +825,8 @@ describe('AgentClientAgentPort', () => { }); describe('resolvePolymorphicType', () => { - let fetchSpy: jest.SpyInstance; - - beforeEach(() => { - fetchSpy = jest.spyOn(globalThis, 'fetch').mockImplementation(jest.fn()); - }); - - afterEach(() => { - fetchSpy.mockRestore(); - }); - - function linkageResponse(data: unknown) { - return new Response(JSON.stringify({ data: { relationships: { commentable: { data } } } }), { - status: 200, - }); - } - - it('reads the linkage type/id and projects the relation on the by-id route', async () => { - fetchSpy.mockResolvedValue(linkageResponse({ type: 'orders', id: '99' })); + it('delegates to agent-client getRelationLinkage on the source collection', async () => { + mockCollection.getRelationLinkage.mockResolvedValue({ type: 'orders', id: '99' }); const result = await port.resolvePolymorphicType( { collection: 'comments', id: [7], relation: 'commentable' }, @@ -849,27 +834,25 @@ describe('AgentClientAgentPort', () => { ); expect(result).toEqual({ type: 'orders', id: '99' }); - - const [url, options] = fetchSpy.mock.calls[0]; - expect(url).toContain('http://localhost:3310/forest/comments/7?'); - expect(url).toContain(`${encodeURIComponent('fields[comments]')}=commentable`); - expect(url).toContain(`${encodeURIComponent('fields[commentable]')}=id`); - expect(options.headers.Authorization).toMatch(/^Bearer /); + expect(mockCollection.getRelationLinkage).toHaveBeenCalledWith([7], 'commentable'); }); - it('joins composite ids with "|" in the by-id route', async () => { - fetchSpy.mockResolvedValue(linkageResponse({ type: 'orders', id: '1|2' })); + it('passes composite ids through to the linkage read', async () => { + mockCollection.getRelationLinkage.mockResolvedValue({ type: 'orders', id: '1|2' }); await port.resolvePolymorphicType( { collection: 'comments', id: ['tenant-1', 5], relation: 'commentable' }, user, ); - expect(fetchSpy.mock.calls[0][0]).toContain('/forest/comments/tenant-1|5?'); + expect(mockCollection.getRelationLinkage).toHaveBeenCalledWith( + ['tenant-1', 5], + 'commentable', + ); }); it('returns null when the relation has no linkage', async () => { - fetchSpy.mockResolvedValue(linkageResponse(null)); + mockCollection.getRelationLinkage.mockResolvedValue(null); const result = await port.resolvePolymorphicType( { collection: 'comments', id: [7], relation: 'commentable' }, @@ -879,8 +862,8 @@ describe('AgentClientAgentPort', () => { expect(result).toBeNull(); }); - it('throws AgentPortError when the agent responds non-2xx', async () => { - fetchSpy.mockResolvedValue(new Response(null, { status: 500 })); + it('wraps agent errors in AgentPortError', async () => { + mockCollection.getRelationLinkage.mockRejectedValue(new Error('boom')); await expect( port.resolvePolymorphicType( From eafb74814f0fe835a8837b99891c49a993326970 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 10 Jun 2026 17:52:38 +0200 Subject: [PATCH 2/4] docs(agent-client): trim polymorphic linkage comments [PRD-493] Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/agent-client/src/domains/collection.ts | 5 ++--- packages/agent-client/src/http-requester.ts | 3 +-- .../src/adapters/agent-client-agent-port.ts | 3 +-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/agent-client/src/domains/collection.ts b/packages/agent-client/src/domains/collection.ts index cbc0439ec9..beb02791a9 100644 --- a/packages/agent-client/src/domains/collection.ts +++ b/packages/agent-client/src/domains/collection.ts @@ -179,9 +179,8 @@ export default class Collection extends CollectionChart { }); } - // Reads a to-one relation's raw JSON:API linkage ({ type, id }) via a `@@@id` projection. - // Used for polymorphic relations where the target `type` is the discriminator: the deserializer - // drops relationship types, so the raw body is read. Returns null when there is no linked record. + // Raw JSON:API linkage { type, id } of a to-one relation (via a `@@@id` projection) — + // needed for polymorphic relations, where the deserializer drops the `type`. Null if no target. async getRelationLinkage( id: RecordId, relation: string, diff --git a/packages/agent-client/src/http-requester.ts b/packages/agent-client/src/http-requester.ts index 5fdadd85a3..11a7d45f3d 100644 --- a/packages/agent-client/src/http-requester.ts +++ b/packages/agent-client/src/http-requester.ts @@ -34,8 +34,7 @@ export default class HttpRequester { query?: Record; maxTimeAllowed?: number; contentType?: 'application/json' | 'text/csv'; - // Return the raw JSON:API body instead of deserializing. The deserializer drops relationship - // `type`, so callers needing the polymorphic discriminator read the raw linkage themselves. + // Return the raw JSON:API body (skip deserialization, which drops relationship `type`). raw?: boolean; }): Promise { try { diff --git a/packages/workflow-executor/src/adapters/agent-client-agent-port.ts b/packages/workflow-executor/src/adapters/agent-client-agent-port.ts index 9e3038c333..b618999d9f 100644 --- a/packages/workflow-executor/src/adapters/agent-client-agent-port.ts +++ b/packages/workflow-executor/src/adapters/agent-client-agent-port.ts @@ -247,8 +247,7 @@ export default class AgentClientAgentPort implements AgentPort { }); } - // Reads the polymorphic relation's target via agent-client's raw linkage read (the deserializer - // drops relationship `type`). agent-client owns URL/auth/projection — no hand-rolled fetch here. + // Delegates to the lib's raw linkage read — agent-client owns URL/auth/projection. async resolvePolymorphicType( { collection, id, relation }: ResolvePolymorphicTypeQuery, user: StepUser, From c2a0cdf1dc78d23090257fe0eb52110ab1c53d06 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 11 Jun 2026 10:55:27 +0200 Subject: [PATCH 3/4] refactor(agent-client): use getOne(raw) instead of a bespoke linkage method [PRD-493] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review feedback: - reuse getOne (add a `raw` option) rather than a dedicated getRelationLinkage; - keep agent-client generic — return the raw JSON:API body and let the consumer extract the polymorphic linkage, instead of baking { type, id } parsing into the lib; - serialization stays in QuerySerializer (getOne -> serialize -> formatFields). The executor adapter reads the raw body via getOne(..., { raw: true }) and parses data.relationships..data itself. Validated e2e (Image -> User / Product). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../agent-client/src/domains/collection.ts | 29 ++++---------- .../test/domains/collection.test.ts | 38 +++++++------------ .../src/adapters/agent-client-agent-port.ts | 20 ++++++++-- .../adapters/agent-client-agent-port.test.ts | 29 +++++++++----- 4 files changed, 57 insertions(+), 59 deletions(-) diff --git a/packages/agent-client/src/domains/collection.ts b/packages/agent-client/src/domains/collection.ts index beb02791a9..94cc73f003 100644 --- a/packages/agent-client/src/domains/collection.ts +++ b/packages/agent-client/src/domains/collection.ts @@ -171,34 +171,21 @@ export default class Collection extends CollectionChart { }); } - async getOne(id: RecordId, options?: SelectOptions): Promise { + // `raw` returns the undeserialized JSON:API body (the deserializer drops relationship `type`, + // which callers reading a polymorphic relation's linkage need). + async getOne( + id: RecordId, + options?: SelectOptions, + { raw = false }: { raw?: boolean } = {}, + ): Promise { return this.httpRequester.query({ method: 'get', path: `/forest/${this.name}/${serializeRecordId(id)}`, query: QuerySerializer.serialize(options, this.name), + raw, }); } - // Raw JSON:API linkage { type, id } of a to-one relation (via a `@@@id` projection) — - // needed for polymorphic relations, where the deserializer drops the `type`. Null if no target. - async getRelationLinkage( - id: RecordId, - relation: string, - ): Promise<{ type: string; id: string } | null> { - const body = await this.httpRequester.query<{ - data?: { relationships?: Record }; - }>({ - method: 'get', - path: `/forest/${this.name}/${serializeRecordId(id)}`, - query: QuerySerializer.serialize({ fields: [`${relation}@@@id`] }, this.name), - raw: true, - }); - - const linkage = body?.data?.relationships?.[relation]?.data; - - return linkage?.type ? { type: String(linkage.type), id: String(linkage.id) } : null; - } - private getActionInfo( actionEndpoints: ActionEndpointsByCollection, collectionName: string, diff --git a/packages/agent-client/test/domains/collection.test.ts b/packages/agent-client/test/domains/collection.test.ts index 9241ce8e5e..af6a2ff116 100644 --- a/packages/agent-client/test/domains/collection.test.ts +++ b/packages/agent-client/test/domains/collection.test.ts @@ -174,38 +174,28 @@ describe('Collection', () => { }); }); - describe('getRelationLinkage', () => { - it('reads the raw linkage via a @@@id projection and returns { type, id }', async () => { - httpRequester.query.mockResolvedValue({ - data: { relationships: { commentable: { data: { type: 'orders', id: '99' } } } }, - }); + describe('getOne', () => { + it('deserializes by default (raw not set)', async () => { + httpRequester.query.mockResolvedValue({ id: 7, name: 'Alice' }); - const result = await collection.getRelationLinkage(7, 'commentable'); + const result = await collection.getOne(7); expect(httpRequester.query).toHaveBeenCalledWith( - expect.objectContaining({ method: 'get', path: '/forest/users/7', raw: true }), + expect.objectContaining({ method: 'get', path: '/forest/users/7', raw: false }), ); - expect(result).toEqual({ type: 'orders', id: '99' }); + expect(result).toEqual({ id: 7, name: 'Alice' }); }); - it('pipe-encodes composite ids', async () => { - httpRequester.query.mockResolvedValue({ - data: { relationships: { commentable: { data: { type: 'orders', id: '1|2' } } } }, - }); - - await collection.getRelationLinkage([1, 'abc'], 'commentable'); - - expect(httpRequester.query).toHaveBeenCalledWith( - expect.objectContaining({ path: '/forest/users/1|abc', raw: true }), - ); - }); + it('returns the raw JSON:API body when raw is set (e.g. to read a relationship type)', async () => { + const body = { + data: { relationships: { commentable: { data: { type: 'orders', id: '99' } } } }, + }; + httpRequester.query.mockResolvedValue(body); - it('returns null when there is no linked record', async () => { - httpRequester.query.mockResolvedValue({ - data: { relationships: { commentable: { data: null } } }, - }); + const result = await collection.getOne(7, { fields: ['commentable@@@id'] }, { raw: true }); - expect(await collection.getRelationLinkage(7, 'commentable')).toBeNull(); + expect(httpRequester.query).toHaveBeenCalledWith(expect.objectContaining({ raw: true })); + expect(result).toEqual(body); }); }); diff --git a/packages/workflow-executor/src/adapters/agent-client-agent-port.ts b/packages/workflow-executor/src/adapters/agent-client-agent-port.ts index b618999d9f..62964cb4ab 100644 --- a/packages/workflow-executor/src/adapters/agent-client-agent-port.ts +++ b/packages/workflow-executor/src/adapters/agent-client-agent-port.ts @@ -247,14 +247,26 @@ export default class AgentClientAgentPort implements AgentPort { }); } - // Delegates to the lib's raw linkage read — agent-client owns URL/auth/projection. + // Resolves a polymorphic relation's target from the raw JSON:API linkage. The deserializer drops + // relationship `type`, so we read the raw body (getOne `raw`) via a `@@@id` projection + // and extract the linkage here — agent-client stays generic (URL/auth/serialization). async resolvePolymorphicType( { collection, id, relation }: ResolvePolymorphicTypeQuery, user: StepUser, ): Promise<{ type: string; id: string } | null> { - return this.callAgent('resolvePolymorphicType', async () => - this.createClient(user).collection(collection).getRelationLinkage(id, relation), - ); + return this.callAgent('resolvePolymorphicType', async () => { + const body = await this.createClient(user) + .collection(collection) + .getOne<{ + data?: { + relationships?: Record; + }; + }>(id, { fields: [`${relation}@@@id`] }, { raw: true }); + + const linkage = body?.data?.relationships?.[relation]?.data; + + return linkage?.type ? { type: String(linkage.type), id: String(linkage.id) } : null; + }); } // Hits GET /forest/ (public, no auth required across all agent versions). A 4xx here means diff --git a/packages/workflow-executor/test/adapters/agent-client-agent-port.test.ts b/packages/workflow-executor/test/adapters/agent-client-agent-port.test.ts index e3a6de5e63..8654af8297 100644 --- a/packages/workflow-executor/test/adapters/agent-client-agent-port.test.ts +++ b/packages/workflow-executor/test/adapters/agent-client-agent-port.test.ts @@ -26,7 +26,6 @@ function createMockClient() { list: jest.fn(), getOne: jest.fn(), update: jest.fn(), - getRelationLinkage: jest.fn(), relation: jest.fn().mockReturnValue(mockRelation), action: jest.fn().mockResolvedValue(mockAction), }; @@ -825,8 +824,12 @@ describe('AgentClientAgentPort', () => { }); describe('resolvePolymorphicType', () => { - it('delegates to agent-client getRelationLinkage on the source collection', async () => { - mockCollection.getRelationLinkage.mockResolvedValue({ type: 'orders', id: '99' }); + function linkageBody(data: unknown) { + return { data: { relationships: { commentable: { data } } } }; + } + + it('reads the raw linkage via getOne and extracts { type, id }', async () => { + mockCollection.getOne.mockResolvedValue(linkageBody({ type: 'orders', id: '99' })); const result = await port.resolvePolymorphicType( { collection: 'comments', id: [7], relation: 'commentable' }, @@ -834,25 +837,31 @@ describe('AgentClientAgentPort', () => { ); expect(result).toEqual({ type: 'orders', id: '99' }); - expect(mockCollection.getRelationLinkage).toHaveBeenCalledWith([7], 'commentable'); + // raw projection read on the source record, parsing done here in the adapter. + expect(mockCollection.getOne).toHaveBeenCalledWith( + [7], + { fields: ['commentable@@@id'] }, + { raw: true }, + ); }); - it('passes composite ids through to the linkage read', async () => { - mockCollection.getRelationLinkage.mockResolvedValue({ type: 'orders', id: '1|2' }); + it('passes composite ids through to the raw read', async () => { + mockCollection.getOne.mockResolvedValue(linkageBody({ type: 'orders', id: '1|2' })); await port.resolvePolymorphicType( { collection: 'comments', id: ['tenant-1', 5], relation: 'commentable' }, user, ); - expect(mockCollection.getRelationLinkage).toHaveBeenCalledWith( + expect(mockCollection.getOne).toHaveBeenCalledWith( ['tenant-1', 5], - 'commentable', + { fields: ['commentable@@@id'] }, + { raw: true }, ); }); it('returns null when the relation has no linkage', async () => { - mockCollection.getRelationLinkage.mockResolvedValue(null); + mockCollection.getOne.mockResolvedValue(linkageBody(null)); const result = await port.resolvePolymorphicType( { collection: 'comments', id: [7], relation: 'commentable' }, @@ -863,7 +872,7 @@ describe('AgentClientAgentPort', () => { }); it('wraps agent errors in AgentPortError', async () => { - mockCollection.getRelationLinkage.mockRejectedValue(new Error('boom')); + mockCollection.getOne.mockRejectedValue(new Error('boom')); await expect( port.resolvePolymorphicType( From 9bbb98d0b5039447ff8eab4e159b5060cf81cbd6 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 11 Jun 2026 11:24:17 +0200 Subject: [PATCH 4/4] refactor(agent-client): rename raw option to skipDeserialization [PRD-493] Review feedback (Scra3): rename the `raw` query/getOne option to the more explicit `skipDeserialization`, and drop the now-redundant comment on getOne. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../agent-client/src/domains/collection.ts | 6 ++---- packages/agent-client/src/http-requester.ts | 7 +++---- .../test/domains/collection.test.ts | 20 ++++++++++++++----- .../src/adapters/agent-client-agent-port.ts | 2 +- .../adapters/agent-client-agent-port.test.ts | 4 ++-- 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/packages/agent-client/src/domains/collection.ts b/packages/agent-client/src/domains/collection.ts index 94cc73f003..7a2c520466 100644 --- a/packages/agent-client/src/domains/collection.ts +++ b/packages/agent-client/src/domains/collection.ts @@ -171,18 +171,16 @@ export default class Collection extends CollectionChart { }); } - // `raw` returns the undeserialized JSON:API body (the deserializer drops relationship `type`, - // which callers reading a polymorphic relation's linkage need). async getOne( id: RecordId, options?: SelectOptions, - { raw = false }: { raw?: boolean } = {}, + { skipDeserialization = false }: { skipDeserialization?: boolean } = {}, ): Promise { return this.httpRequester.query({ method: 'get', path: `/forest/${this.name}/${serializeRecordId(id)}`, query: QuerySerializer.serialize(options, this.name), - raw, + skipDeserialization, }); } diff --git a/packages/agent-client/src/http-requester.ts b/packages/agent-client/src/http-requester.ts index 11a7d45f3d..1cc8c54476 100644 --- a/packages/agent-client/src/http-requester.ts +++ b/packages/agent-client/src/http-requester.ts @@ -26,7 +26,7 @@ export default class HttpRequester { query, maxTimeAllowed, contentType, - raw, + skipDeserialization, }: { method: 'get' | 'post' | 'put' | 'delete'; path: string; @@ -34,8 +34,7 @@ export default class HttpRequester { query?: Record; maxTimeAllowed?: number; contentType?: 'application/json' | 'text/csv'; - // Return the raw JSON:API body (skip deserialization, which drops relationship `type`). - raw?: boolean; + skipDeserialization?: boolean; }): Promise { try { const url = this.buildUrl(path); @@ -51,7 +50,7 @@ export default class HttpRequester { const response = await req; - if (raw) { + if (skipDeserialization) { return response.body as Data; } diff --git a/packages/agent-client/test/domains/collection.test.ts b/packages/agent-client/test/domains/collection.test.ts index af6a2ff116..8df256bae2 100644 --- a/packages/agent-client/test/domains/collection.test.ts +++ b/packages/agent-client/test/domains/collection.test.ts @@ -175,26 +175,36 @@ describe('Collection', () => { }); describe('getOne', () => { - it('deserializes by default (raw not set)', async () => { + it('deserializes by default (skipDeserialization not set)', async () => { httpRequester.query.mockResolvedValue({ id: 7, name: 'Alice' }); const result = await collection.getOne(7); expect(httpRequester.query).toHaveBeenCalledWith( - expect.objectContaining({ method: 'get', path: '/forest/users/7', raw: false }), + expect.objectContaining({ + method: 'get', + path: '/forest/users/7', + skipDeserialization: false, + }), ); expect(result).toEqual({ id: 7, name: 'Alice' }); }); - it('returns the raw JSON:API body when raw is set (e.g. to read a relationship type)', async () => { + it('returns the raw JSON:API body when skipDeserialization is set', async () => { const body = { data: { relationships: { commentable: { data: { type: 'orders', id: '99' } } } }, }; httpRequester.query.mockResolvedValue(body); - const result = await collection.getOne(7, { fields: ['commentable@@@id'] }, { raw: true }); + const result = await collection.getOne( + 7, + { fields: ['commentable@@@id'] }, + { skipDeserialization: true }, + ); - expect(httpRequester.query).toHaveBeenCalledWith(expect.objectContaining({ raw: true })); + expect(httpRequester.query).toHaveBeenCalledWith( + expect.objectContaining({ skipDeserialization: true }), + ); expect(result).toEqual(body); }); }); diff --git a/packages/workflow-executor/src/adapters/agent-client-agent-port.ts b/packages/workflow-executor/src/adapters/agent-client-agent-port.ts index 62964cb4ab..fc1cd1fbf7 100644 --- a/packages/workflow-executor/src/adapters/agent-client-agent-port.ts +++ b/packages/workflow-executor/src/adapters/agent-client-agent-port.ts @@ -261,7 +261,7 @@ export default class AgentClientAgentPort implements AgentPort { data?: { relationships?: Record; }; - }>(id, { fields: [`${relation}@@@id`] }, { raw: true }); + }>(id, { fields: [`${relation}@@@id`] }, { skipDeserialization: true }); const linkage = body?.data?.relationships?.[relation]?.data; diff --git a/packages/workflow-executor/test/adapters/agent-client-agent-port.test.ts b/packages/workflow-executor/test/adapters/agent-client-agent-port.test.ts index 8654af8297..4e5489e04c 100644 --- a/packages/workflow-executor/test/adapters/agent-client-agent-port.test.ts +++ b/packages/workflow-executor/test/adapters/agent-client-agent-port.test.ts @@ -841,7 +841,7 @@ describe('AgentClientAgentPort', () => { expect(mockCollection.getOne).toHaveBeenCalledWith( [7], { fields: ['commentable@@@id'] }, - { raw: true }, + { skipDeserialization: true }, ); }); @@ -856,7 +856,7 @@ describe('AgentClientAgentPort', () => { expect(mockCollection.getOne).toHaveBeenCalledWith( ['tenant-1', 5], { fields: ['commentable@@@id'] }, - { raw: true }, + { skipDeserialization: true }, ); });