feat(actors ls): add --public / --private flags to filter actors by visibility - #1362
Open
kuntal1461 wants to merge 1 commit into
Open
feat(actors ls): add --public / --private flags to filter actors by visibility#1362kuntal1461 wants to merge 1 commit into
kuntal1461 wants to merge 1 commit into
Conversation
…isibility - Add mutually exclusive --public and --private boolean flags - When either flag is active, silently paginate all actors (100/page) client-side using Actor.isPublic from the full Actor object, since the Apify API does not expose an isPublic query parameter on GET /v2/acts - Sort the full filtered set first, then apply --limit / --offset so pagination always operates on a consistently ordered result - Default limit on the filter path is the full matched set (not 20), so `actors ls --private` returns all private actors without requiring the user to know and supply --limit manually - Preserve correct JSON metadata (total, offset, limit, count, desc) for both filter and no-filter paths - Refactor hydration into a reusable private hydrateActors() method - Add e2e tests: public-only filter, private-only filter, limit+metadata assertions, mutual-exclusion rejection - Regenerate docs/reference.md via pnpm update-docs Closes apify#1361
kuntal1461
requested review from
DaveHanns,
l2ysho and
szaganek
as code owners
August 28, 2026 08:43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds two mutually exclusive flags —
--publicand--private— toapify actors ls. When either flag is provided the command silently paginates through all actors, filters by visibility client-side, sorts, then applies--limit/--offseton the result. The no-filter path (existing behavior) is completely unchanged.Closes #1361.
Problem
apify actors lsandapify actors ls --myreturn a mixed list of public and private actors. There was no way to ask "show me only my private actors" from the CLI; users had to open the Apify Console and filter manually.What users experienced:
Specifically painful for:
Why the existing flags were insufficient:
--my,--limit,--offset, and--descall operate on raw API pagination; none of them exposeActor.isPublic. The Apify API (GET /v2/acts) does not accept anisPublicquery parameter, so filtering had to be client-side.Root Cause
The
ActorCollectionListOptionstype inapify-clientoffersmy,desc,limit, andoffset— no visibility filter. The fullActorobject (fromclient.actor(id).get()) does exposeisPublic, but the collection list endpoint returns lightweightActorCollectionListItemobjects that omit it.Before this PR, the command fetched one page of results and rendered them directly. There was no mechanism to:
isPublicat all.Solution
Two new boolean flags (
--public,--private) are declared with the framework'sexclusiveconstraint so providing both is a hard error.When either flag is active, a different execution path runs:
Key design decisions:
--offset/--limitare applied after sorting the full filtered set. Applying them before sorting would return the wrong page when actors span multiple API pages.--limitis not provided,jsonLimitis set tosortedMatching.length(not 20). Since all actors are already in memory, capping silently at 20 would meanapify actors ls --privatereturns only 20 of 45 private actors with no indication there are more — a silent data loss. A user who wants 20 can still pass--limit 20.client.actors().list({ limit: limit ?? 20, offset: offset ?? 0, ... })single-page path is not modified. Its default of 20 is correct because it relies on API-level pagination.Architecture / Design
Existing pattern
ActorsLsCommand.run()already owned the full fetch → hydrate → sort → render pipeline. The hydration step (fetching the fullActorobject and last run per item) was inlined directly inrun()as an anonymousPromise.allblock.What changed
hydrateActors()extracted to a private method (src/commands/actors/ls.ts):hydrateActors()was already effectively present as an inlinePromise.all— this just gives it a name and signature so both paths can call it without duplicating the Actor + runs fetch logic. No new abstraction layer was introduced; the method lives on the same class and follows the existing private-method pattern (sortByModifiedAt,sortByLastRun).INTERNAL_PAGE_SIZE = 100is a private static constant on the class, consistent with how other internal constants are declared in the codebase. It controls the per-page fetch size of the pagination loop in the filter path and is not exposed to users.Two-path
run()keepsjsonTotal,jsonOffset,jsonLimitas hoistedletvariables populated by whichever path runs, so the JSON output block and empty-state block at the bottom ofrun()can use them correctly without duplication.The change does not introduce any new file, module, or dependency.
Impact
User impact
Users can now filter by visibility directly from the terminal:
Functional impact
Compatibility impact
Fully backward-compatible. The two new flags are additive. All existing invocations (
apify actors ls,apify actors ls --my,--limit,--offset,--desc,--json) use the no-filter path and are not affected.One subtle behavioural difference in the no-filter path:
--limitand--offsetno longer carry framework-level defaults (default: 0/default: 20removed from flag declarations). The values0and20are now applied explicitly in code (offset ?? 0,limit ?? 20) to avoid ambiguity between "user did not pass the flag" and "user passed 0/20". This does not change observable behavior but fixes a latent issue where the filter path could not distinguish an explicit--limit 20from the default.Performance impact
The filter path makes additional API calls (one
GET /v2/actsper page + oneGET /v2/acts/{id}per actor). This is inherent to the design: the API does not support server-side visibility filtering. The no-filter path makes the same number of calls as before.Security impact
None. No authentication, authorization, or credential-handling code is touched.
Operational impact
None. No configuration, deployment, or environment changes required.
What This Fixes
apify actors lscould not filter by actor visibility.total,offset,limit, andcountmetadata.--limit/--offseton a filtered result set now always apply to a consistently sorted set (sort-before-slice).--publicand--privatetogether produce a clear error message rather than silent incorrect behavior.Testing
Build and static analysis:
E2E tests added (
test/e2e/commands/actors/ls.test.ts):filters to public actors with --public flagactor.isPublic === truefilters to private actors with --private flagactor.isPublic === falserespects --limit flag(strengthened)parsed.items.length ≤ 5ANDparsed.limit === 5— previously only checked JSON parsability--private --limit returns correct metadatalimit,offset,totalare internally consistentrejects --public and --private used together"cannot also be provided"Before / After
Filtering by visibility — Before:
After:
JSON metadata — Before (filter path did not exist; no-filter path):
{ "total": 80, "count": 20, "offset": 0, "limit": 20 }After (
--private, no limit, 45 private actors):{ "total": 45, "count": 45, "offset": 0, "limit": 45 }After (
--private --limit 10):{ "total": 45, "count": 10, "offset": 0, "limit": 10 }Risk / Trade-offs
Additional API calls in filter path: Fetching all actors + full Actor objects is O(n) in the number of actors. For accounts with hundreds of actors this is slow. This is a known trade-off: the Apify API does not expose server-side visibility filtering. A future API change adding
?isPublic=truetoGET /v2/actswould allow eliminating the pagination loop entirely.No streaming: Results appear only after all pages are fetched and filtered. For large accounts, the command may appear to hang. A progress indicator could be added in a follow-up.
Concurrent hydration:
hydrateActors()callsclient.actor(id).get()and.runs().list()concurrently viaPromise.all. For the filter path this runs across all actors in a page (100). This should not cause rate-limit issues in normal usage but is worth noting.Overall the change is low-risk: the no-filter path is untouched, the filter path is entirely new code, and the flags are additive.
Files Changed
src/commands/actors/ls.ts--public/--privateflags withexclusivemutual-exclusion constraint.INTERNAL_PAGE_SIZE = 100static constant.hydrateActors()private method (was inlinePromise.allinrun()).run(): pagination loop, client-side filter, sort-before-slice.jsonTotal,jsonOffset,jsonLimitso both paths feed a shared JSON/empty-state block.--limit/--offsetflags; apply defaults explicitly in code.test/e2e/commands/actors/ls.test.ts--limittest: now assertsparsed.limitvalue, not just JSON parsability.docs/reference.mdpnpm run update-docs. Now shows[--private | --public]inapify actors lsusage.Maintainer Notes
The filter path is intentionally kept separate from the no-filter path rather than merging them into a single flow. The two paths have fundamentally different pagination semantics: the no-filter path delegates pagination to the API (one call, API enforces limit/offset), while the filter path must fetch all data first (because the API cannot filter) and then paginate client-side. Merging them would add conditional complexity throughout the flow for a small DRY gain. The current structure makes it easy to replace the filter path with a server-side call if the API ever gains
?isPublicsupport, without touching the no-filter path at all.