feat(datasource-pylon): messages / conversation thread (EXT-9) - #358
Conversation
1 new issue
|
| # message field, and the thread is not even part of the payload the | ||
| # search endpoint returns. | ||
| def define_thread_field | ||
| add_field('messages', ColumnSchema.new(column_type: [Issue::MESSAGE_THREAD_SCHEMA], |
There was a problem hiding this comment.
🟡 Medium issue/schema_definition.rb:80
A projection containing only messages:body_html returns every issue column and every message attribute instead of only the requested nested field. BaseCollection#project treats colon-qualified projections as having no scalar fields, and embed_foreign assigns the complete serialized record without applying the nested projection; the same leak affects relation projections such as account:name. Update projection handling at both levels so nested-only projections retain the requested fields.
Also found in 2 other location(s)
packages/forest_admin_datasource_pylon/lib/forest_admin_datasource_pylon/collections/issue/messages_embedder.rb:24
want_messages?accepts a nested-only projection such as['messages:body_html'], butBaseCollection#projecttreats a projection containing only colon-qualified entries as having no scalar fields and returns the entire issue record. The resulting row therefore exposes every issue column in addition tomessages, instead of honoring the requested projection. The existing spec masks this by always includingid; nested-only structured-column projections trigger the leak.
packages/forest_admin_datasource_pylon/lib/forest_admin_datasource_pylon/collections/relation_embedder.rb:35
embed_foreignassigns the complete serialized foreign record torow[name]without applying the nested projection. For a projection such asaccount:name, the response therefore includes every account field (for example domains, CRM settings, and custom fields) instead of onlyname; when the projection contains only relation fields,BaseCollection#projectalso returns the complete source record because its scalarwantedset is empty. Relation/detail responses consequently over-return unrequested data.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/forest_admin_datasource_pylon/lib/forest_admin_datasource_pylon/collections/issue/schema_definition.rb around line 80:
A projection containing only `messages:body_html` returns every issue column and every message attribute instead of only the requested nested field. `BaseCollection#project` treats colon-qualified projections as having no scalar fields, and `embed_foreign` assigns the complete serialized record without applying the nested projection; the same leak affects relation projections such as `account:name`. Update projection handling at both levels so nested-only projections retain the requested fields.
Also found in 2 other location(s):
- packages/forest_admin_datasource_pylon/lib/forest_admin_datasource_pylon/collections/issue/messages_embedder.rb:24 -- `want_messages?` accepts a nested-only projection such as `['messages:body_html']`, but `BaseCollection#project` treats a projection containing only colon-qualified entries as having no scalar fields and returns the entire issue record. The resulting row therefore exposes every issue column in addition to `messages`, instead of honoring the requested projection. The existing spec masks this by always including `id`; nested-only structured-column projections trigger the leak.
- packages/forest_admin_datasource_pylon/lib/forest_admin_datasource_pylon/collections/relation_embedder.rb:35 -- `embed_foreign` assigns the complete serialized foreign record to `row[name]` without applying the nested projection. For a projection such as `account:name`, the response therefore includes every account field (for example domains, CRM settings, and custom fields) instead of only `name`; when the projection contains only relation fields, `BaseCollection#project` also returns the complete source record because its scalar `wanted` set is empty. Relation/detail responses consequently over-return unrequested data.
Ports the Zendesk comments embedder to Pylon: the messages of an issue are
read through GET /issues/{id}/messages and embedded as a structured array
column on PylonIssue.
The thread is asked for without a limit, so Pylon answers with the whole
conversation in one request; a page would have handed back the oldest
messages and cut the recent ones off. Authors are flattened from the payload
Pylon already nests in each message, so no author lookup is spent at all.
The fan-out is bounded like the primary-key lookups of this collection: the
endpoint allows 20 requests per minute and a thread costs one request per
row, so rows past MAX_MESSAGE_EMBEDS are left at nil -- unknown, never the
empty list, which would read as "this issue has no message". A thread that
cannot be read degrades the same way instead of failing the page.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
define_thread_field builds its ColumnSchema by hand, and ColumnSchema defaults is_groupable to true, where the add_column of BaseCollection passes false for every Pylon column: no endpoint aggregates, and the pages of a cursor walk are not the dataset, so a chart would answer a fraction as if it were the whole collection. The thread is a worse candidate still -- an array type, absent from the search payload, and embedded one request per row at read time. Caught by the "declares no column groupable" spec EXT-8 added, which this branch only meets now that it sits on top of it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
c45d504 to
f940b27
Compare
64857f4
into
feat/datasource-pylon
Closes EXT-9 — Story 5 of EXT-4.
What
The conversation of an issue was invisible: opening a
PylonIssueshowed the initialbody_htmland nothing of the replies and internal notes that followed. This ports the Zendesk comments embedder — the thread is read throughGET /issues/{id}/messagesand embedded as a structured array columnmessagesonPylonIssue.client.rbfetch_issue_messages+ thecollect_pages/best_efforthelperscollections/issue/messages_embedder.rbcollections/issue.rbMESSAGE_THREAD_SCHEMA,MAX_MESSAGE_EMBEDS, wiring intolistcollections/issue/schema_definition.rbmessagescolumn, neither filterable nor sortableThree decisions worth reviewing
No
limitis sent. Pylon then answers with the whole thread in a single request. Messages come back oldest first, so asking for a page would have handed back the oldest ones and cut the recent ones off — the half of a conversation nobody opens a ticket to read. The cursor is still followed defensively, bounded byMAX_COLLECTED_PAGES, in case Pylon starts paginating on its own.No author lookup at all. Unlike Zendesk, which only gives an
author_id, Pylon already nestsauthor: {name, avatar_url, contact:{id,email}, user:{id,email}}in every message. The story's "bulk-resolve message authors" is therefore satisfied without a single extra request: the payload is flattened intoauthor_name/author_email/author_avatar_url/author_contact_id/author_user_id. Both ids are kept, since Pylon puts the contact and user sides side by side with nothing telling them apart.The fan-out is bounded, and truncation reads as unknown. One thread is one request against an endpoint allowing 20 per minute, so a page projecting the column is capped at
MAX_MESSAGE_EMBEDS(10) likeMAX_ID_LOOKUPSbounds the primary-key fan-out. Rows past the cap — and rows whose thread failed to be read — are left atnil, never at[], which would read as "this issue has no message". A degraded thread costs the operator a column, not the records they opened.One deviation from the plan:
want_messages?requires the projection to name the column. A nil projection — what a count and an export go through — embeds nothing, exactly likeRelationEmbedder, rather than spending one request per row on a path that never asked for the conversation.Field names follow the columns of the collection (
body_html,created_at) rather than the payload (message_html,timestamp), so the operator does not have two conventions to reconcile inside one schema.Verification
461 examples, 0 failures— coverage 934/934 lines (100%, threshold 90). RuboCop clean over the 49 files of the package.Specs cover: the column schema shape; one request on a record detail with the thread in order; the
message_html/timestamprenames; contact-authored vs agent-authored vs author-less messages; a projection withoutmessagesissuing zero requests;messages:body_htmlreaching inside the column; an empty thread as[]; the cap leaving rows atnilwith a warning; and a 500 leaving the page served with the column atnil.🤖 Generated with Claude Code
Note
Add message thread embedding to the Pylon issue collection
fetch_issue_messages(issue_id)toClient, which walks cursor-paginated Pylon endpoints to collect full message threads, capping atMAX_COLLECTED_PAGES(10) with a warning.messagesfield to thePylonIssueschema (array of structured message objects, not filterable or sortable) viaSchemaDefinition.MessagesEmbeddermodule populatesmessagesper row when the projection requests it, flattening author contact/user fields into a flatauthor_*structure.MAX_MESSAGE_EMBEDS(10) rows per page; rows past the cap receivenilwith a truncation warning. Failed fetches also degrade tonilrather than raising.Macroscope summarized f940b27.