Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@

## Unreleased

- Added `templates.instantiate_template` and the ticket-release lifecycle on
`events` (`list_ticket_releases`, `update_ticket_releases`, and
`close_ticket_release`). Template instantiation sends `{}` when no overrides
are supplied, URI-escapes identifiers, and uses exact-response replay;
ticket-release writes remain single-attempt.

- **Security/reliability:** Mutations now default to a single attempt. Automatic header-replay
retries are limited to chart create/copy, event create, and workspace create, preventing
retries are limited to chart create/copy, template instantiation, event create, and workspace create, preventing
transient failures from duplicating holds or best-available results and from issuing extra
show-once credentials.
- Aligned event, inventory, webhook, manage-session, and Designer requests with the generated
Expand Down
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ require "seatlayer"

client = SeatLayer::Client.new(ENV.fetch("SEATLAYER_SECRET_KEY"))

# 1. Provision a venue for a new organiser from one of your templates.
chart = client.charts.copy("c_template_arena")["meta"]
# 1. Provision a venue for a new organiser from a public template.
chart = client.templates.instantiate_template("arena-standard")["meta"]
client.charts.publish(chart["id"])

# 2. Create an event on it.
Expand Down Expand Up @@ -252,11 +252,12 @@ error carries `status`, `code`, `body` and `request_id` — quote the request id
## Reliability

**Retries.** Reads (`GET`/`HEAD`) retry 429, 408 and 5xx with exponential backoff and full jitter;
`Retry-After` wins when the server sends it. Automatic mutation retries are limited to the four
operations backed by exact response replay: `charts.create`, `charts.copy`, `events.create`, and
`workspaces.create`. Other 4xx responses are never retried.
`Retry-After` wins when the server sends it. Automatic mutation retries are limited to the five
operations backed by exact response replay: `charts.create`, `charts.copy`,
`templates.instantiate_template`, `events.create`, and `workspaces.create`. Other 4xx responses
are never retried.

**Idempotency.** Those four replay-backed operations carry an `Idempotency-Key`, generated when you
**Idempotency.** Those five replay-backed operations carry an `Idempotency-Key`, generated when you
do not supply one and reused across attempts. Other mutations are single-attempt and receive no
automatic key. A caller-supplied key is forwarded but does not enable retries. This includes
inventory holds and bookings, show-once credential or secret creation, unsupported operations, and
Expand Down Expand Up @@ -289,7 +290,8 @@ client.request("POST", "/v1/events/ev_1/some-new-route", body: { "qty" => 2 })
| Resource | Methods |
| --- | --- |
| `charts` | `list` `list_all` `create` `retrieve` `update` `delete` `copy` `archive` `unarchive` `publish` |
| `events` | `list` `list_all` `create` `retrieve` `update` `delete` `update_poster` `delete_poster` `update_chart` `close` `reopen` `archive` `retrieve_hold_ttl` `update_hold_ttl` `retrieve_report` `retrieve_log` |
| `templates` | `instantiate_template` |
| `events` | `list` `list_all` `create` `retrieve` `update` `delete` `update_poster` `delete_poster` `update_chart` `close` `reopen` `archive` `retrieve_hold_ttl` `update_hold_ttl` `list_ticket_releases` `update_ticket_releases` `close_ticket_release` `retrieve_report` `retrieve_log` |
| `inventory` | `hold` `hold_best_available` `book_best_available` `extend_hold` `retrieve_hold` `release` `book` `box_office_book` `unbook` `list_bookings` `retrieve_booking` `block` `unblock` `unblock_all` `retrieve_availability` `update_availability` |
| `channels` | `list_channels` `create_channel` `update_channel` `update_assignments` `list_allocation` `retrieve_access_preview` `retrieve_report` `pause` `unpause` `archive` `create_buyer_access_session` `list_buyer_access_sessions` `revoke_buyer_access_session` `create_access_link` `list_access_links` `rotate_access_link` `revoke_access_link` |
| `sessions` | `create_manage_session` `revoke_manage_session` `create_designer_session` `revoke_designer_session` |
Expand Down Expand Up @@ -337,7 +339,7 @@ the public manifest, not just from the wrapper.
| Node.js (server) | [`@seatlayer/server`](https://www.npmjs.com/package/@seatlayer/server) |
| Python (server) | [`seatlayer`](https://pypi.org/project/seatlayer/) |
| PHP (server) | [`seatlayer/seatlayer-php`](https://packagist.org/packages/seatlayer/seatlayer-php) |
| Java (server) | [`io.seatlayer:seatlayer-java`](https://central.sonatype.com/artifact/io.seatlayer/seatlayer-java/0.1.0) |
| Java (server) | [`io.seatlayer:seatlayer-java`](https://central.sonatype.com/artifact/io.seatlayer/seatlayer-java/0.3.0) |
| Go (server) | [`github.com/seatlayer/seatlayer-go`](https://pkg.go.dev/github.com/seatlayer/seatlayer-go) |
| .NET (server) | [`SeatLayer`](https://www.nuget.org/packages/SeatLayer) |

Expand Down
3 changes: 2 additions & 1 deletion lib/seatlayer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
module SeatLayer
# The SeatLayer client.
class Client
attr_reader :charts, :events, :inventory, :channels, :sessions, :webhooks, :workspaces
attr_reader :charts, :events, :inventory, :channels, :sessions, :webhooks, :workspaces, :templates

def initialize(secret_key, base_url: HTTPClient::DEFAULT_BASE_URL,
max_retries: HTTPClient::DEFAULT_MAX_RETRIES,
Expand All @@ -32,6 +32,7 @@ def initialize(secret_key, base_url: HTTPClient::DEFAULT_BASE_URL,
@inventory = Inventory.new(@http)
@channels = Channels.new(@http)
@sessions = Sessions.new(@http)
@templates = Templates.new(@http)
@webhooks = Webhooks.new(@http)
@workspaces = Workspaces.new(@http)
end
Expand Down
32 changes: 32 additions & 0 deletions lib/seatlayer/resources.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,23 @@ def publish(chart_id)
end
end

# Published SeatLayer catalogue templates.
#
# Instantiation creates an independent draft chart. Publish that returned
# chart before creating an event from it.
class Templates < Resource
# Instantiate a public template into a new draft chart.
#
# +fields+ intentionally defaults to an empty Hash: the API requires a JSON
# object even when there are no overrides, and JSON.generate({}) is `{}`.
def instantiate_template(template_id, fields: {}, idempotency_key: nil)
@client.post(
"/v1/templates/#{encode(template_id)}/instantiate", fields,
idempotency_key: idempotency_key, retry_policy: :header_replay
)
end
end

# Event lifecycle, metadata and reports.
class Events < Resource
# One page of events.
Expand Down Expand Up @@ -225,6 +242,21 @@ def update_hold_ttl(event_key, hold_ttl_ms)
@client.post("/v1/events/#{encode(event_key)}/hold-ttl", { "holdTtlMs" => hold_ttl_ms })
end

def list_ticket_releases(event_key)
@client.get("/v1/events/#{encode(event_key)}/releases")
end

# Replace every ticket release for an event. This stays single-attempt: the
# route does not promise exact idempotent-response replay.
def update_ticket_releases(event_key, releases:)
@client.put("/v1/events/#{encode(event_key)}/releases", { "releases" => releases })
end

# Close one release while preserving its audit provenance.
def close_ticket_release(event_key, release_id)
@client.post("/v1/events/#{encode(event_key)}/releases/#{encode(release_id)}/close")
end

def retrieve_report(event_key)
@client.get("/v1/events/#{encode(event_key)}/report")
end
Expand Down
2 changes: 1 addition & 1 deletion lib/seatlayer/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module SeatLayer
VERSION = "0.3.0"
VERSION = "0.4.0"
end
40 changes: 40 additions & 0 deletions spec/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,46 @@
end

describe "generated public wire contract" do
it "instantiates templates with an empty object and exact-response replay" do
client, transport = build_client([
{ status: 429, body: '{"error":"rate_limited"}',
headers: { "retry-after" => "0" } },
{ status: 201, body: '{"meta":{"id":"c_1"}}' }
])
client.templates.instantiate_template("arena/standard")

expect(transport.calls).to have_attributes(length: 2)
expect(transport.calls[0].url).to end_with("/v1/templates/arena%2Fstandard/instantiate")
expect(transport.calls[0].body).to eq("{}")
expect(transport.calls[0].headers["Idempotency-Key"])
.to eq(transport.calls[1].headers["Idempotency-Key"])
end

it "wraps ticket release routes with replacement bodies and single-attempt writes" do
client, transport = build_client([
{ status: 200, body: '{"releases":[]}' },
{ status: 200, body: '{"releases":[]}' },
{ status: 429, body: '{"error":"rate_limited"}',
headers: { "retry-after" => "0" } }
])
client.events.list_ticket_releases("ev/1")
client.events.update_ticket_releases(
"ev/1", releases: [{ "id" => "rel_1", "name" => "Early", "price" => 2500, "action" => "buy" }]
)

expect { client.events.close_ticket_release("ev/1", "rel/1") }
.to raise_error(SeatLayer::RateLimitError)

expect(transport.calls).to have_attributes(length: 3)
expect(transport.calls[0].http_method).to eq("GET")
expect(transport.calls[0].url).to end_with("/v1/events/ev%2F1/releases")
expect(JSON.parse(transport.calls[1].body)).to eq(
"releases" => [{ "id" => "rel_1", "name" => "Early", "price" => 2500, "action" => "buy" }]
)
expect(transport.calls[2].url).to end_with("/v1/events/ev%2F1/releases/rel%2F1/close")
expect(transport.calls[2].headers).not_to have_key("Idempotency-Key")
end

it "sends the expanded event, hold, and block fields" do
client, transport = build_client([
{ status: 201, body: '{"meta":{"key":"ev_1"}}' },
Expand Down