Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
9cd45e0
feat(datasource-pylon): foundation — gem, config & resilient client (…
christophebrun-forest Aug 7, 2026
8b804dd
feat(datasource-pylon): PylonIssue read-only collection with cursor p…
christophebrun-forest Aug 10, 2026
af03bac
feat(datasource-pylon): filters, search and PK short-circuit hardenin…
christophebrun-forest Aug 12, 2026
28d4da6
fix(pylon): register the gem for release and declare activesupport
christophebrun-forest Aug 18, 2026
4b14086
feat(datasource-pylon): all collections + relations (EXT-8) (#354)
christophebrun-forest Aug 18, 2026
64857f4
feat(datasource-pylon): messages / conversation thread (EXT-9) (#358)
christophebrun-forest Aug 18, 2026
1c92286
ci: bound every job and harden the unixODBC install (#360)
christophebrun-forest Aug 19, 2026
9f80673
feat(datasource-pylon): custom fields introspection (EXT-10) (#359)
christophebrun-forest Aug 19, 2026
46e1e5b
feat(datasource-pylon): write operations (CRUD) (EXT-11) (#362)
christophebrun-forest Aug 21, 2026
0fd0ed7
feat(datasource-pylon): close and create-with-notification action plu…
christophebrun-forest Aug 24, 2026
837988d
refactor(customizer): build the sort schema in one pass and trim its …
christophebrun-forest Aug 24, 2026
8f99395
Merge branch 'main' into feat/datasource-pylon
christophebrun-forest Aug 25, 2026
442e116
feat(datasource-pylon): proactive throttling (EXT-13) (#373)
christophebrun-forest Aug 25, 2026
ab4630a
feat(datasource-pylon): bound the boot-time custom-fields introspecti…
christophebrun-forest Aug 26, 2026
65190b9
chore(pylon): merge main into the integration branch
christophebrun-forest Aug 26, 2026
b9cb7d1
fix(pylon): three defects Macroscope found on the branch
christophebrun-forest Aug 26, 2026
a93474b
fix(pylon): page a lookup naming many issues by id
christophebrun-forest Aug 26, 2026
2495109
fix(pylon): refuse two plugin configurations that misbehave silently
christophebrun-forest Aug 26, 2026
cc01551
fix(pylon): keep no column for a projection naming only relations
christophebrun-forest Aug 26, 2026
373f94d
refactor(pylon): drop the countable flag nothing opts into
christophebrun-forest Aug 26, 2026
7033ba1
fix(pylon): stop the custom-field introspection at its first failure
christophebrun-forest Aug 26, 2026
bd55e8b
docs(pylon): add the package README
christophebrun-forest Aug 26, 2026
2a1b4d6
fix(pylon): serve the whole record for an empty projection
christophebrun-forest Aug 27, 2026
67cd786
fix(pylon): refuse an internal note the form never offered
christophebrun-forest Aug 27, 2026
1fd03cb
fix(pylon): keep a select with no option left read-only
christophebrun-forest Aug 27, 2026
96a0575
docs(pylon): record what bounds CloseIssue and what does not
christophebrun-forest Aug 27, 2026
0a2d4e4
chore(pylon): align version.rb with the other packages
christophebrun-forest Aug 27, 2026
d75bab2
fix(pylon): intersect the id conditions of an and
christophebrun-forest Aug 27, 2026
524cc95
fix(pylon): let a refused selection reach the operator
christophebrun-forest Aug 27, 2026
b6bd085
docs(pylon): correct which scope bounds CloseIssue
christophebrun-forest Aug 27, 2026
89917dc
fix(pylon): honour the projection of an embedded relation
christophebrun-forest Aug 28, 2026
901b5da
fix(pylon): keep the api key out of every inspect
christophebrun-forest Aug 28, 2026
a0cc00d
fix(pylon): refuse a read body Pylon answered without data
christophebrun-forest Aug 28, 2026
6db19e0
fix(pylon): stop a cursor walk on a cursor already followed
christophebrun-forest Aug 28, 2026
1eb6a93
feat(pylon): count the collections Pylon hands back whole
christophebrun-forest Aug 28, 2026
723b90f
fix(toolkit): keep the aggregator of a nested condition tree branch
christophebrun-forest Aug 28, 2026
3e38d09
fix(pylon): bound what one run of the close issue action writes
christophebrun-forest Aug 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
- forest_admin_datasource_snowflake
- forest_admin_datasource_mambu_payments
- forest_admin_datasource_graphql_hasura
- forest_admin_datasource_pylon

steps:
- name: Checkout
Expand Down Expand Up @@ -88,6 +89,7 @@ jobs:
- forest_admin_datasource_snowflake
- forest_admin_datasource_mambu_payments
- forest_admin_datasource_graphql_hasura
- forest_admin_datasource_pylon
services:
mongodb:
image: mongo:latest
Expand Down Expand Up @@ -166,7 +168,7 @@ jobs:
with:
verbose: true
oidc: true
files: ${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_agent/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_active_record/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_customizer/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_toolkit/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_rails/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_mongoid/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_rpc_agent/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_rpc/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_zendesk/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_snowflake/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_mambu_payments/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_graphql_hasura/coverage.json
files: ${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_agent/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_active_record/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_customizer/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_toolkit/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_rails/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_mongoid/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_rpc_agent/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_rpc/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_zendesk/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_snowflake/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_mambu_payments/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_graphql_hasura/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_pylon/coverage.json

deploy:
name: Release package
Expand Down
7 changes: 5 additions & 2 deletions .releaserc.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ module.exports = {
'sed -i \'s/VERSION = ".*"/VERSION = "${nextRelease.version}"/g\' packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/version.rb; '+
'sed -i \'s/VERSION = ".*"/VERSION = "${nextRelease.version}"/g\' packages/forest_admin_datasource_snowflake/lib/forest_admin_datasource_snowflake/version.rb; '+
'sed -i \'s/VERSION = ".*"/VERSION = "${nextRelease.version}"/g\' packages/forest_admin_datasource_mambu_payments/lib/forest_admin_datasource_mambu_payments/version.rb; '+
'sed -i \'s/VERSION = ".*"/VERSION = "${nextRelease.version}"/g\' packages/forest_admin_datasource_graphql_hasura/lib/forest_admin_datasource_graphql_hasura/version.rb; ',
'sed -i \'s/VERSION = ".*"/VERSION = "${nextRelease.version}"/g\' packages/forest_admin_datasource_graphql_hasura/lib/forest_admin_datasource_graphql_hasura/version.rb; '+
'sed -i \'s/VERSION = ".*"/VERSION = "${nextRelease.version}"/g\' packages/forest_admin_datasource_pylon/lib/forest_admin_datasource_pylon/version.rb; ',
successCmd:
'( cd packages/forest_admin_agent && gem build && gem push forest_admin_agent-*.gem );' +
'( cd packages/forest_admin_datasource_active_record && gem build && gem push forest_admin_datasource_active_record-*.gem );' +
Expand All @@ -45,7 +46,8 @@ module.exports = {
'( cd packages/forest_admin_datasource_zendesk && gem build && gem push forest_admin_datasource_zendesk-*.gem );' +
'( cd packages/forest_admin_datasource_snowflake && gem build && gem push forest_admin_datasource_snowflake-*.gem );' +
'( cd packages/forest_admin_datasource_mambu_payments && gem build && gem push forest_admin_datasource_mambu_payments-*.gem );' +
Comment on lines 47 to +48

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High .releaserc.js:47

A failed forest_admin_datasource_mambu_payments build or push is reported as a successful successCmd when the subsequent Pylon publish succeeds, so semantic-release can mark a partially published release as successful. The Mambu subshell is separated from the Pylon subshell with ;, making Pylon's status the overall status; chain them with && so the Mambu failure propagates.

Suggested change
'( cd packages/forest_admin_datasource_mambu_payments && gem build && gem push forest_admin_datasource_mambu_payments-*.gem );' +
'( cd packages/forest_admin_datasource_mambu_payments && gem build && gem push forest_admin_datasource_mambu_payments-*.gem ) &&' +
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @.releaserc.js around line 47:

A failed `forest_admin_datasource_mambu_payments` build or push is reported as a successful `successCmd` when the subsequent Pylon publish succeeds, so semantic-release can mark a partially published release as successful. The Mambu subshell is separated from the Pylon subshell with `;`, making Pylon's status the overall status; chain them with `&&` so the Mambu failure propagates.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid, but not this PR's to fix.

All fourteen packages in successCmd are chained with ;, and have been since long before this branch — the mambu/pylon boundary is just where the diff happens to touch the list. Changing only that one separator would leave the other twelve masking failures exactly as they do today, so the fix belongs in a repo-wide change against main.

The underlying point stands and is worth its own ticket: a gem push failing anywhere in the chain is currently reported as a successful release.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — fixing all fourteen packages consistently makes more sense than patching just the diff boundary. Would you like me to open a PR against main that chains all the successCmd gem operations with && instead of ; so any build or push failure properly propagates?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed as EXT-19 so it outlives this thread — all fourteen packages, as a change against main.

'( cd packages/forest_admin_datasource_graphql_hasura && gem build && gem push forest_admin_datasource_graphql_hasura-*.gem );' ,
'( cd packages/forest_admin_datasource_graphql_hasura && gem build && gem push forest_admin_datasource_graphql_hasura-*.gem );' +
'( cd packages/forest_admin_datasource_pylon && gem build && gem push forest_admin_datasource_pylon-*.gem );' ,
},
],
[
Expand All @@ -68,6 +70,7 @@ module.exports = {
'packages/forest_admin_datasource_snowflake/lib/forest_admin_datasource_snowflake/version.rb',
'packages/forest_admin_datasource_mambu_payments/lib/forest_admin_datasource_mambu_payments/version.rb',
'packages/forest_admin_datasource_graphql_hasura/lib/forest_admin_datasource_graphql_hasura/version.rb',
'packages/forest_admin_datasource_pylon/lib/forest_admin_datasource_pylon/version.rb',
'package.json'
],
},
Expand Down
7 changes: 7 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Gemspec/RequireMFA:
- 'packages/forest_admin_datasource_snowflake/forest_admin_datasource_snowflake.gemspec'
- 'packages/forest_admin_datasource_mambu_payments/forest_admin_datasource_mambu_payments.gemspec'
- 'packages/forest_admin_datasource_graphql_hasura/forest_admin_datasource_graphql_hasura.gemspec'
- 'packages/forest_admin_datasource_pylon/forest_admin_datasource_pylon.gemspec'

# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
Expand Down Expand Up @@ -133,6 +134,7 @@ Style/MutableConstant:
- 'packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/version.rb'
- 'packages/forest_admin_datasource_snowflake/lib/forest_admin_datasource_snowflake/version.rb'
- 'packages/forest_admin_datasource_mambu_payments/lib/forest_admin_datasource_mambu_payments/version.rb'
- 'packages/forest_admin_datasource_pylon/lib/forest_admin_datasource_pylon/version.rb'

# Offense count: 38
# This cop supports safe autocorrection (--autocorrect).
Expand Down Expand Up @@ -217,6 +219,7 @@ Style/StringLiterals:
- 'packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/version.rb'
- 'packages/forest_admin_datasource_snowflake/lib/forest_admin_datasource_snowflake/version.rb'
- 'packages/forest_admin_datasource_mambu_payments/lib/forest_admin_datasource_mambu_payments/version.rb'
- 'packages/forest_admin_datasource_pylon/lib/forest_admin_datasource_pylon/version.rb'

# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
Expand Down Expand Up @@ -262,6 +265,8 @@ Metrics/ParameterLists:
- 'packages/forest_admin_agent/lib/forest_admin_agent/audit_trail/store.rb'
- 'packages/forest_admin_datasource_graphql_hasura/lib/forest_admin_datasource_graphql_hasura/configuration.rb'
- 'packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb'
- 'packages/forest_admin_datasource_pylon/lib/forest_admin_datasource_pylon/collections/base_collection.rb'
- 'packages/forest_admin_datasource_pylon/lib/forest_admin_datasource_pylon/configuration.rb'
- 'packages/forest_admin_datasource_snowflake/lib/forest_admin_datasource_snowflake/datasource.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/routes/query_handler.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb'
Expand Down Expand Up @@ -294,6 +299,7 @@ Metrics/ModuleLength:
- 'packages/forest_admin_datasource_customizer/spec/**/*'
- 'packages/forest_admin_datasource_zendesk/spec/**/*'
- 'packages/forest_admin_datasource_mambu_payments/spec/**/*'
- 'packages/forest_admin_datasource_pylon/spec/**/*'
- 'packages/forest_admin_rails/spec/**/*'
- 'packages/forest_admin_rpc_agent/spec/**/*'
- 'packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/helpers.rb'
Expand Down Expand Up @@ -369,6 +375,7 @@ Metrics/ClassLength:
- 'packages/forest_admin_datasource_graphql_hasura/lib/forest_admin_datasource_graphql_hasura/query/query_builder.rb'
- 'packages/forest_admin_datasource_graphql_hasura/lib/forest_admin_datasource_graphql_hasura/query/aggregator.rb'
- 'packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb'
- 'packages/forest_admin_datasource_pylon/lib/forest_admin_datasource_pylon/collections/base_collection.rb'
- 'packages/forest_admin_datasource_snowflake/lib/forest_admin_datasource_snowflake/collection.rb'
- 'packages/forest_admin_datasource_snowflake/lib/forest_admin_datasource_snowflake/datasource.rb'
- 'packages/forest_admin_datasource_snowflake/lib/forest_admin_datasource_snowflake/utils/query.rb'
Expand Down
8 changes: 8 additions & 0 deletions packages/forest_admin_datasource_pylon/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*.gem
.bundle/
Gemfile.lock
Gemfile-test.lock
coverage/
pkg/
tmp/
.rspec_status
3 changes: 3 additions & 0 deletions packages/forest_admin_datasource_pylon/.rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--format documentation
--color
--require spec_helper
16 changes: 16 additions & 0 deletions packages/forest_admin_datasource_pylon/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
source 'https://rubygems.org'

gemspec

gem 'forest_admin_datasource_customizer'
gem 'forest_admin_datasource_toolkit'
gem 'rake', '~> 13.0'
gem 'rubocop', '1.86.1'
gem 'rubocop-performance', '1.26.1'
gem 'rubocop-rspec', '3.9.0'

group :development, :test do
gem 'rspec', '~> 3.0'
gem 'simplecov', '~> 0.22', require: false
gem 'webmock', '~> 3.0'
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 17 lines of identical code in 4 locations (mass = 76) [qlty:identical-code]

19 changes: 19 additions & 0 deletions packages/forest_admin_datasource_pylon/Gemfile-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
source 'https://rubygems.org'

# Specify your gem's dependencies in forest_admin_datasource_pylon.gemspec
gemspec

gem 'rake', '~> 13.0'
gem 'rubocop', '1.86.1'
gem 'rubocop-performance', '1.26.1'
gem 'rubocop-rspec', '3.9.0'

group :development, :test do
gem 'forest_admin_datasource_customizer', path: '../forest_admin_datasource_customizer'
gem 'forest_admin_datasource_toolkit', path: '../forest_admin_datasource_toolkit'
gem 'rspec', '~> 3.0'
gem 'simplecov', '~> 0.22', require: false
gem 'simplecov-html', '~> 0.12.3'
gem 'simplecov_json_formatter', '~> 0.1.4'
gem 'webmock', '~> 3.0'
end
159 changes: 159 additions & 0 deletions packages/forest_admin_datasource_pylon/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Forest — Pylon datasource

Surface [Pylon](https://usepylon.com) issues, accounts, contacts, users and teams as Forest
collections, with filters, free-text search, relations, the conversation thread, custom fields,
CRUD writes and two action plugins.

## Installation

```ruby
# Gemfile
gem 'forest_admin_datasource_pylon'
```

## Usage

```ruby
# app/lib/forest_admin_rails/create_agent.rb
ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(
ForestAdminDatasourcePylon::Datasource.new(api_key: ENV['PYLON_API_KEY'])
)
```

A single Bearer token is the whole configuration. `GET /me` is the health check: it returns the
organization owning the token, which is enough to prove the credentials are usable.

## Collections

| Collection | Pylon resource | Read | Write |
| --- | --- | --- | --- |
| `PylonIssue` | `/issues` | `POST /issues/search`, cursor-paginated | create / update / delete |
| `PylonAccount` | `/accounts` | search + list, cursor-paginated | create / update / delete |
| `PylonContact` | `/contacts` | search + list, cursor-paginated | create / update / delete |
| `PylonUser` | `/users` | whole dataset, unpaginated | update |
| `PylonTeam` | `/teams` | whole dataset, unpaginated | create / update |

`PylonIssue`, `PylonAccount` and `PylonContact` also carry their Pylon custom fields, introspected
at boot from `GET /custom-fields`.

## What the API cannot do, and what this does about it

Pylon is a ticketing API, not a database, and several things Forest asks for have no equivalent.
Where that happens this datasource **refuses with a message naming the reason** rather than
answering something that looks right and is not. All of these arrive as a 400 carrying the text.

- **No aggregate endpoint and no total.** `aggregate` is refused on the cursor-backed collections:
counting the pages a walk collected would answer a fraction of a collection as if it were the
whole of it, so those are not advertised as countable. `PylonUser` and `PylonTeam` are the
exception — their endpoint hands back every record, so a count or a group over it is the figure
a server-side aggregation would have given.
- **No sort parameter on `/issues/search`.** Issues always come back newest first. A requested
order is reported in the log rather than silently swallowed.
- **No `id` filter on the search endpoints.** A primary-key lookup is short-circuited to
`GET /issues/{id}`, one request per id. That fan-out is capped per page (`MAX_ID_LOOKUPS`), and
a selection naming more ids *and* filtering them further is refused, since which records the
page holds could only be known by reading all of them.
- **No joins.** A condition on a relation is answered by reading the foreign collection for its
keys and sending them as an `in`. Past `MAX_RELATION_KEYS` the condition is refused.
- **Search and id lookup are different endpoints.** Combining a free-text search with a filter on
`id` is refused: neither endpoint can do the other's half.
- **Writes are one record per request.** A write reaching more records than one pass covers is
refused up front; one that fails halfway reports exactly which records were written, so a retry
can target the untouched ones rather than performing the write twice.

## Rate limits

Pylon meters **per endpoint**, not per token, from 30 to 300 requests a minute depending on the
endpoint. `RateLimits` holds the documented budget of every endpoint this client calls, and
`RateLimiter` spaces requests out so each one is spent rather than exceeded — a sliding window per
endpoint, in front of the 429 retry rather than instead of it.

The limiter is a smoother, not a guarantee: past `DEFAULT_MAX_WAIT` a request goes out anyway and
the 429 retry takes over, with one log line per endpoint per window saying so. Under real
saturation — several agents or processes on the same token — the retry is the defence.

To meter on your own side instead, pass `rate_limiter: nil`:

```ruby
ForestAdminDatasourcePylon::Datasource.new(api_key: ENV['PYLON_API_KEY'], rate_limiter: nil)
```

## Boot-time introspection

Custom fields are read while the datasource is being constructed, one call per object type. That
sits in front of a Rails boot, so it runs on its own connection with short timeouts and a single
quick retry (`boot_open_timeout`, `boot_timeout`, `boot_retry_policy`), and the first failure
stops the remaining object types from being tried.

An introspection that fails costs the custom columns, not the datasource: the agent boots on the
native schema and says so in the log.

## Action plugins

### `CloseIssue`

Moves the selected issues to a state, single and bulk, one request per issue with a per-id rescue
so one failure does not abort the batch.

```ruby
@agent.collection :Ticket do |collection|
collection.use(ForestAdminDatasourcePylon::Plugins::CloseIssue, {
datasource: pylon_datasource, # required
issue_id_field: 'pylon_issue_id', # omit when the action sits on PylonIssue itself
state: 'closed', # default
scopes: %i[single bulk] # default
})
end
```

Two things to know before granting it.

**Which scope bounds it.** The state is written straight through the Pylon client rather than
through `PylonIssue`, because the action is registered on the host collection — but the ids were
read before that write, through the collection the action sits on, and the agent intersects the
operator's scope into the filter that read them. So the scope that bounds this is the scope of the
**host** collection: mounted on `PylonIssue`, a scope or segment on `PylonIssue` bounds exactly what
it closes; mounted on a business collection with `issue_id_field`, what bounds it is the records the
operator may see there, and the issue ids those records carry. In that second form the column is
the authority — an operator who can write it can name any Pylon issue — so treat it as one.

**The batch is capped at `MAX_TARGETS` (20) issues**, the budget a filter-driven write already gets:
Pylon takes one request per issue, so a wider selection is a long run of sequential writes the
request may time out on, leaving the issues closed up to that point closed and reporting which ones
to nobody. Past the cap the run is refused before its first write, the way `update` and `delete`
refuse a selection costing more than `MAX_WRITE_REQUESTS`. The cap counts the issues named, not the
records selected: a column of issue ids is not a key, so a hundred host records naming ten issues is
a batch of ten.

### `CreateIssueWithNotification`

Creates an issue from a form and notifies the requester, optionally writing the new issue id back
onto the host record.

```ruby
@agent.collection :Customer do |collection|
collection.use(ForestAdminDatasourcePylon::Plugins::CreateIssueWithNotification, {
datasource: pylon_datasource, # required
sender_email: 'support@example.com', # required when the destination is email
issue_id_field: 'pylon_issue_id', # optional id writeback
email_templates: [{ title: 'Outage', content: '<p>Sorry {{ record.name }}</p>' }]
})
end
```

`{{ record.field }}` tokens are interpolated from the host record, HTML-escaped in the message
body. Template titles must be unique and cannot be `"No template"`, which names the option that
picks none of them.

The message body is HTML the operator writes and Pylon delivers to the requester, and the
requester address is a free-text field. Restrict both actions to the roles that should be able to
send mail on your organization's behalf.

## Development

```bash
cd packages/forest_admin_datasource_pylon
bundle install
bundle exec rspec
bundle exec rubocop
```
6 changes: 6 additions & 0 deletions packages/forest_admin_datasource_pylon/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'

RSpec::Core::RakeTask.new(:spec)

task default: :spec
Loading
Loading