Getting started

Pagination

Every list surface has three methods: list for one page, list_all for everything, iterate for a stream.

Every paginated collection in the SDK is exposed three ways. They hit the same endpoint and differ only in how much they buffer.

MethodReturnsRequestsUse when
listEnvless::PageExactly oneYou are rendering a page, or you only need a count
list_allArrayAs many as it takesYou need the whole set in memory anyway
iteratenil with a block, an Enumerator without oneAs many as it takes, lazilyThe set is large, or you can stop early
ruby
require "envless/sdk"

page = Envless.products.list(limit: 50)

puts page.items.size
puts "#{page.pagination[:limit]} #{page.pagination[:offset]}"
puts "#{page.total_count} #{page.has_more?}"
ruby
require "envless/sdk"

everything = Envless.products.list_all

puts everything.size
ruby
require "envless/sdk"

Envless.products.iterate do |product|
  break if product[:slug] == "billing"

  puts product[:name]
end

iterate is the only one that can stop early. Breaking out of the block stops making requests, so scanning for one item in a large workspace costs one page, not all of them.

Without a block, iterate returns an Enumerator that fetches the next page only when it gets there, so the whole Enumerable toolbox works on a stream: first(10) makes one request, and find stops at the first match.

ruby
require "envless/sdk"

first_ten = Envless.products.iterate.first(10)
billing = Envless.products.iterate.find do |product|
  product[:slug] == "billing"
end

puts first_ten.size
puts billing[:name] if billing

An Envless::Page is a Data value with items and pagination, plus has_more? and total_count, and its RBS type is generic over the item, so page.items is an Array[product_resource] to Steep.

Options

List methods take these as keyword arguments, after their positional slugs. Each list accepts only the options its route reads, so an option a route ignores raises ArgumentError:

OptionTypeNotes
limit:Integer1 to 100. Defaults to 25 on the API. iterate and list_all default to 100 to minimise round trips, and clamp anything higher.
offset:IntegerRows to skip. iterate and list_all use it as a starting point and then advance it themselves.
search:String or SymbolProducts, product projects, projects, environments, invites, members, roles, keys and webhooks. Each method page says what it matches.
order:String or Symbolname, asc or desc, on products, product projects, projects, environments, keys and webhooks.
sort_by:String or SymbolRoles only: name (the default), updated or created.
role_id:StringMembers only: keeps the members holding that role.
status:String or SymbolWebhook deliveries only: pending, processing, succeeded or failed.
product:String or SymbolProjects, environments, variables, variable history and versions: disambiguates a project slug that exists in more than one product.
timeout:Integer, Float or RationalBounds each request of the walk, not the whole walk.

A limit: or offset: that is not an Integer raises ArgumentError before anything is sent. A limit: outside 1 to 100 is refused by the API with 400 request.invalid_pagination rather than being clamped server-side, and error.field names the offending parameter.

How the walk terminates

list_all and iterate stop on either of two conditions, which matters because it makes them safe against a server that keeps saying hasMore:

hasMore is false

The normal case.

A page comes back empty

Even if hasMore is still true. Without this a bad cursor would loop forever.

The offset advances by the number of items actually received, not by limit, so a short page does not skip rows. A failure on any page fails the whole list_all call, and the rows it had already collected are lost; iterate has already yielded the earlier items, so it raises at the page that failed.

Secondary collections

A namespace can paginate more than one collection. Those get their own trio, named after what they read.

NamespacePrimarySecondary
productslist, list_all, iteratelist_projects, list_all_projects, iterate_projects and access.list, access.list_all, access.iterate
variableslist, list_all, iteratelist_history, list_all_history, iterate_history
versionslist, list_all, iteratelist_entries, list_all_entries, iterate_entries
webhookslist, list_all, iteratelist_deliveries, list_all_deliveries, iterate_deliveries

Two endpoints look like lists but are not paginated, because the whole set is small and fixed. Both return a plain Hash rather than a page:

MethodReturns
roles.list_permissionspermissions_resource with the full catalogue, a byResource index, and the system role sets
webhooks.list_event_typesevent_types_resource with eventTypes and totalCount

One collection carries an extra field

products.access.list returns an Envless::ProductAccessPage, which is a normal page plus the product's privacy flag, read with private?. That flag tells you whether the grants matter at all: on a public product every member has access and the list is informational.

ruby
require "envless/sdk"

access = Envless.products.access.list("internal-tools")

unless access.private?
  puts "Public product, every member already has access"
end

puts access.items.map { |entry| entry[:id] }

access.list_all and access.iterate only stream items, so they drop the flag. If you need it, use access.list.

Counting without fetching

total_count is the size of the full filtered set, not the page, so a single one-row request is enough to count.

ruby
require "envless/sdk"

puts "#{Envless.members.list(limit: 1).total_count} members"