FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat(actors ls): add --public / --private flags to filter actors by visibility by kuntal1461 · Pull Request #1362 · apify/apify-cli · GitHub

feat(actors ls): add --public / --private flags to filter actors by visibility - #1362

Open
kuntal1461 wants to merge 1 commit into
apify:masterfrom
kuntal1461:feat/actors-ls-public-private-filter
Open

feat(actors ls): add --public / --private flags to filter actors by visibility#1362
kuntal1461 wants to merge 1 commit into
apify:masterfrom
kuntal1461:feat/actors-ls-public-private-filter

Conversation

Copy link
Copy Markdown
Contributor

Summary

Adds two mutually exclusive flags — --public and --private — to apify actors ls. When either flag is provided the command silently paginates through all actors, filters by visibility client-side, sorts, then applies --limit / --offset on the result. The no-filter path (existing behavior) is completely unchanged.

Closes #1361.


Problem

apify actors ls and apify actors ls --my return 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:

$ apify actors ls --my
# Returns a mix of public and private — no way to narrow it

Specifically painful for:

  • Auditing which actors are accidentally public or private
  • Shell-script automation ("process only private actors")
  • CI pipelines that need to assert visibility state

Why the existing flags were insufficient:
--my, --limit, --offset, and --desc all operate on raw API pagination; none of them expose Actor.isPublic. The Apify API (GET /v2/acts) does not accept an isPublic query parameter, so filtering had to be client-side.


Root Cause

The ActorCollectionListOptions type in apify-client offers my, desc, limit, and offset — no visibility filter. The full Actor object (from client.actor(id).get()) does expose isPublic, but the collection list endpoint returns lightweight ActorCollectionListItem objects that omit it.

Before this PR, the command fetched one page of results and rendered them directly. There was no mechanism to:

  1. Fetch across all pages to find every matching actor.
  2. Inspect isPublic at all.
User: apify actors ls --my
         ↓
client.actors().list({ limit, offset, my })   ← single page, no visibility info
         ↓
Render mixed list

Solution

Two new boolean flags (--public, --private) are declared with the framework's exclusive constraint so providing both is a hard error.

When either flag is active, a different execution path runs:

User: apify actors ls --my --private
         ↓
Filter path activated
         ↓
Pagination loop: client.actors().list({ limit: 100, offset }) until all pages fetched
         ↓
hydrateActors(): client.actor(id).get() for each item → Actor.isPublic available
         ↓
Client-side filter: keep items where isPublic === false
         ↓
sortByModifiedAt() / sortByLastRun() on the full matched set
         ↓
slice(offset, offset + limit) applied after sort
         ↓
Render or emit JSON with correct metadata

Key design decisions:

  • Sort-before-slice: --offset/--limit are applied after sorting the full filtered set. Applying them before sorting would return the wrong page when actors span multiple API pages.
  • Default limit = full matched set: In the filter path, when --limit is not provided, jsonLimit is set to sortedMatching.length (not 20). Since all actors are already in memory, capping silently at 20 would mean apify actors ls --private returns 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.
  • No-filter path untouched: the existing 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 full Actor object and last run per item) was inlined directly in run() as an anonymous Promise.all block.

What changed

hydrateActors() extracted to a private method (src/commands/actors/ls.ts):

ActorsLsCommand
  └── run()
        ├── [filter path]   pagination loop → hydrateActors() → filter → sort → slice
        └── [no-filter path] single page   → hydrateActors() → sort
  └── hydrateActors()       reused by both paths
  └── sortByModifiedAt()    unchanged
  └── sortByLastRun()       unchanged

hydrateActors() was already effectively present as an inline Promise.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 = 100 is 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() keeps jsonTotal, jsonOffset, jsonLimit as hoisted let variables populated by whichever path runs, so the JSON output block and empty-state block at the bottom of run() 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:

apify actors ls --private              # all private actors
apify actors ls --public               # all public actors
apify actors ls --my --private         # your private actors
apify actors ls --private --limit 10   # first 10 private actors
apify actors ls --public --private     # error: mutually exclusive

Functional impact

  • Filter path: visibility filtering, sort-before-slice, unlimited default.
  • No-filter path: behavior identical to before this PR.

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: --limit and --offset no longer carry framework-level defaults (default: 0 / default: 20 removed from flag declarations). The values 0 and 20 are 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 20 from the default.

Performance impact

The filter path makes additional API calls (one GET /v2/acts per page + one GET /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 ls could not filter by actor visibility.
  • JSON output on the filter path now reports accurate total, offset, limit, and count metadata.
  • --limit / --offset on a filtered result set now always apply to a consistently sorted set (sort-before-slice).
  • --public and --private together produce a clear error message rather than silent incorrect behavior.

Testing

Build and static analysis:

pnpm run build   ✅ clean
pnpm run lint    ✅ no violations (oxlint --type-aware)
pnpm run format  ✅ no diff (oxfmt --check)
pnpm run update-docs  ✅ docs/reference.md regenerated

E2E tests added (test/e2e/commands/actors/ls.test.ts):

Test What it proves
filters to public actors with --public flag Every item in the response has actor.isPublic === true
filters to private actors with --private flag Every item has actor.isPublic === false
respects --limit flag (strengthened) parsed.items.length ≤ 5 AND parsed.limit === 5 — previously only checked JSON parsability
--private --limit returns correct metadata limit, offset, total are internally consistent
rejects --public and --private used together Exit code ≠ 0, stderr contains "cannot also be provided"

Before / After

Filtering by visibility — Before:

$ apify actors ls --my
# Mixed list, no way to see only private actors

After:

$ apify actors ls --my --private
# Only private actors, all of them, sorted by last run
$ apify actors ls --my --private --limit 5
# First 5 private actors
$ apify actors ls --public --private
Error: --private=true cannot also be provided when using --public

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=true to GET /v2/acts would 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() calls client.actor(id).get() and .runs().list() concurrently via Promise.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

  • Added --public / --private flags with exclusive mutual-exclusion constraint.
  • Added INTERNAL_PAGE_SIZE = 100 static constant.
  • Extracted hydrateActors() private method (was inline Promise.all in run()).
  • Added filter path in run(): pagination loop, client-side filter, sort-before-slice.
  • Hoisted jsonTotal, jsonOffset, jsonLimit so both paths feed a shared JSON/empty-state block.
  • Removed framework-level defaults from --limit / --offset flags; apply defaults explicitly in code.

test/e2e/commands/actors/ls.test.ts

  • Strengthened --limit test: now asserts parsed.limit value, not just JSON parsability.
  • Added four new e2e test cases covering the new flags.

docs/reference.md

  • Regenerated by pnpm run update-docs. Now shows [--private | --public] in apify actors ls usage.

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 ?isPublic support, without touching the no-filter path at all.

…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
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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(actors ls): add --public / --private flags to filter actors by visibility

2 participants


Back | FazBrowse Home | New Git URL