Skip to content

20139 - Add filterset search, and the clip poster URL - #10

Open
ghultink wants to merge 3 commits into
masterfrom
feature/filterset-search
Open

20139 - Add filterset search, and the clip poster URL#10
ghultink wants to merge 3 commits into
masterfrom
feature/filterset-search

Conversation

@ghultink

@ghultink ghultink commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Mirrors bb-sapi-php-sdk#20. Same design, same wire format — review that one first if you're reviewing both.

Why

The SDK can list, page and sort media clips, but it cannot filter them. MediaClip.list() takes only limit, offset and sort, and the query layer is Record<string, string> fed to searchParams.set(), which cannot express indexed parameters at all.

So every integration hand-rolls a filter query — and a wrong one fails silently: SAPI ignores it and still answers HTTP 200 with a body carrying neither numfound nor items, indistinguishable from an empty library. The TYPO3 extension's library grid showed no videos for exactly this reason.

What

const filterSet = FilterSet.create()
  .where('status', 'is', 'published')
  .where('title', 'contains', 'koert');

await sdk.mediaclip.search(filterSet);

Groups are AND-ed, filters within a group OR-ed — the structure OVP6 builds (app/services/filter-set.types.ts), mirrored here so a filterset moves between the UI, the API and any SDK unchanged.

The filterset is sent, not compiled

The part worth reviewing. SAPI compiles filtersets itself, via the same SearchRequestHelper that serves the OVP — and that is what OVP6 sends (filter-set.service.ts:609). Compiling in the SDK would be a second implementation of semantics the server owns, free to drift, with a failure mode nobody sees. Multiply by four SDKs and you get four subtly different compilers.

Verified equivalent against a live publication of 5777 clips:

filter numfound
status is published 4361
title contains "koert" 1 — finds Koert Live
hasInteractivity is true 868
published AND video 3679

The fq escape hatch, and its trap

search() still accepts raw filter queries for the rare thing a filterset cannot express. Its encoding is pinned by a test:

  • fq[0]=… — accepted
  • fq=… repeated, or a nested fq[][0]=…ignored, silently, with HTTP 200 and an empty envelope

Also: the clip poster URL

Thumbnail.getMediaClipPosterPath(). A clip's src is its source media file, so building a poster from it yields a link to a .mov — the service replies Invalid src mime type: video/quicktime. The route is /mediaclip/{id}/spthumbnail/{w}/{h}.webp, and a draft clip needs a read-only RPC token (never the write key — this URL ends up in page source).

Checks

  • 154 tests pass (14 new), tsc --noEmit clean
  • No new dependencies

Note on browser use

The SDK is otherwise browser-ready — pure ESM, fetch transport, no Node built-ins — except src/util/hotp.ts, which uses node:crypto and Buffer. That is also the one part that must never run in a browser, since HOTP minting needs the API secret. A browser entry point that omits the RPC authenticator would be a small change if it's ever wanted; not in this PR.

🤖 Generated with Claude Code


Self-review findings — addressed in the latest commit

A thorough adversarial review of this PR (all three SDK PRs plus the TYPO3 consumer) found three behaviour bugs that the original tests pinned as if correct — green CI over broken code:

  1. isEmpty / isNotEmpty were silent no-ops. The backend compiler skips any filter whose value is empty — presence tests included. Verified live: a bare isEmpty returned the full unfiltered publication (5777/5777). OVP6 sends the placeholder * with the comment "backend needs a value to work"; the wire format now does the same (live: author isEmpty 5496, isNotEmpty 278).
  2. Numeric values in ingested filtersets now normalise to decimal strings. SAPI accepts numbers (views > 100 as number and string both return 99), but dropping or mishandling them varied per SDK.
  3. Booleans now normalise to 'true'/'false'. The backend mangles a JSON true into "1", which matches nothing — verified live: 0 results as a boolean vs 868 as the string.

Plus per-SDK hardening (tolerant ingestion of malformed groups, descriptive errors for unknown operators, documented server-side quirks). Details in the fix commit message.

ghultink and others added 2 commits August 19, 2026 17:06
Mirrors bluebillywig/bb-sapi-php-sdk#20. The SDK can list, page and sort media
clips but not filter them, so every integration hand-rolls a filter query — and
because a wrong one fails silently, they hand-roll it wrong and nobody notices.
SAPI ignores a filter it cannot read and still answers HTTP 200 with neither
`numfound` nor `items`, which is indistinguishable from an empty library.

    const filterSet = FilterSet.create()
      .where('status', 'is', 'published')
      .where('title', 'contains', 'koert');

    await sdk.mediaclip.search(filterSet);

Groups are AND-ed, filters within a group OR-ed — the structure OVP6 builds
(app/services/filter-set.types.ts), which this mirrors so a filterset moves
between the UI, the API and any SDK unchanged.

The filterset is SENT, not compiled. SAPI compiles filtersets itself, through
the same SearchRequestHelper that serves the OVP, and that is what OVP6 sends
(filter-set.service.ts:609). Compiling here would be a second implementation of
semantics the server owns, free to drift, with an invisible failure mode.
Verified equivalent against a live publication of 5777 clips: filterset and a
hand-compiled fq return identical counts (published 4361, title contains "koert"
1, hasInteractivity 868, published AND video 3679).

The raw `fq` escape hatch stays for the rare thing a filterset cannot express,
with its encoding pinned: `fq[0]=` is accepted, while a plain `fq=` and a nested
`fq[][0]=` are both ignored without an error.

Also adds Thumbnail.getMediaClipPosterPath(). A clip's `src` is its source media
file, so building a poster from it yields a link to a .mov — the service replies
"Invalid src mime type: video/quicktime". The route is
/mediaclip/{id}/spthumbnail/{w}/{h}.webp, and a draft clip needs a read-only RPC
token.

154 tests pass, tsc clean, no new dependencies.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The repo gates on 100% coverage and filter-set.ts landed at 98.5%. toJSON is
what makes JSON.stringify(filterSet) produce the wire format, which is how a
caller embedding a filterset in a larger body will reach for it, so it is worth
a test rather than a deletion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mirrors the fix commit on bb-sapi-php-sdk#20. Three behaviour bugs, all
confirmed against the live API, all previously pinned by the tests as if
correct — which is why CI was green over broken code.

1. isEmpty/isNotEmpty were silent no-ops. The backend's compiler skips any
   filter whose value is empty — presence tests included — so a bare isEmpty
   returned the full unfiltered publication (verified live: 5777 of 5777).
   OVP6 sends the placeholder '*' with the comment "backend needs a value to
   work"; strip() now does the same, overriding any caller-supplied value.
   Live with the placeholder: author isEmpty 5496, isNotEmpty 278.

2. Numeric values were silently DROPPED: hasValue() accepted only strings, so
   an ingested OVP/Automations filterset with a JSON number lost the filter
   and returned the full result set. SAPI accepts numbers (views > 100 as a
   number and as "100" both return 99 live); they are now normalised to
   decimal strings.

3. Booleans were also dropped — and could not simply be passed through,
   because the backend mangles a JSON true into "1", which matches nothing
   (verified live: hasInteractivity true as a boolean returned 0 results; as
   the string 'true', 868). Booleans now normalise to 'true'/'false'.

Also from the review:

- toArray() no longer crashes on a malformed group (missing filters array):
  from() ingests external JSON, and one junk entry should not take the whole
  filterset down. Non-scalar array members are dropped rather than serialised.
- The exported value type is widened to FilterScalar/FilterValue (accepting
  more is BC-safe); the wire output remains string | string[].
- Documented the server-side quirks a caller inherits: value '0' is dropped
  by the backend's empty-value guard, '+' becomes a space and '"' is
  stripped, and an unknown field returns numfound=0 rather than an error.
- Version bumped to 1.1.0: the filterset feature is additive.

160 tests pass, 100% coverage, tsc clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ghultink ghultink changed the title Add filterset search, and the clip poster URL 20139 - Add filterset search, and the clip poster URL Aug 25, 2026
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.

1 participant