Skip to content

feat(streams): add q search and tag filtering to stream listing - #555

Merged
Xhristin3 merged 2 commits into
XStreamRollz:mainfrom
rozemary2026-a11y:feat/issue-532-stream-search-tag-filtering
Aug 27, 2026
Merged

feat(streams): add q search and tag filtering to stream listing#555
Xhristin3 merged 2 commits into
XStreamRollz:mainfrom
rozemary2026-a11y:feat/issue-532-stream-search-tag-filtering

Conversation

@rozemary2026-a11y

Copy link
Copy Markdown
Contributor

Summary

Closes #532

Adds server-side search and tag filtering to GET /streams: q performs a case-insensitive substring match over stream name and description (LIKE wildcards are escaped so user input matches literally), and tag restricts results to streams carrying a given tag, accepting either a tag slug or a numeric id. The tag value is resolved exactly once in StreamsService through the existing shared tag lookup, so neither repository implementation re-implements slugification.

Why

GET /streams previously supported only status, visibility, and ownerOnly — the dashboard had no way to search streams or filter by tag without fetching everything and filtering client-side, which broke total/hasMore pagination semantics. The SDK exposed the same gap: no listStreams() method and no search/filter parameters on the pagination path.

What was built

File What it contains
api/src/streams/dto/list-streams.query.dto.ts q (trimmed, max 200 chars, wildcards literal) and tag (slug regex-validated) query params with Swagger docs
api/src/streams/streams.service.ts Resolves tag (slug or numeric id) via TagsService.resolveTag; unknown tag → honest empty page; builds a StreamListPredicate with only the defined keys
api/src/streams/repository/streams.repository.ts StreamListPredicate interface; in-memory q/tagId filtering; injects the shared TagsRepository for tag associations
api/src/streams/repository/streams-db.repository.ts SQL name ILIKE $n ESCAPE '\' OR description ILIKE $n with escapeLikePattern(), and an EXISTS (SELECT 1 FROM stream_tags …) join predicate
api/src/tags/tags.service.ts resolveTag() — numeric-id vs slug lookup, returns undefined for unknown tags
api/src/tags/repository/tags.repository.ts, tags-db.repository.ts listStreamIdsForTag() backing the in-memory and SQL tag filters
api/src/tags/tags.module.ts Exports TagsRepository so StreamsModule's in-memory repo can resolve tag associations
app/lib/api/streams.ts, app/hooks/useStreams.ts q/tag pass-through in listStreams() and in the React Query keys
xstreamroll-sdk/src/types.ts, client.ts, index.ts, pagination.ts New StreamListParams, listStreams() method, paginateAll query option, exports
tests/contracts/src/streams.contract.ts list-streams-search and list-streams-by-tag contracts
api/src/contract-provider.spec.ts, xstreamroll-sdk/__tests__/contract.consumer.test.ts Provider verification (seeds a tagged stream, verifies the filtered queries) and consumer verification (asserts the exact query strings the SDK sends)

Tests live alongside each layer: new spec files for the DTO validation, the in-memory repository, and the SQL repository (SQL-string assertions), plus service-level tests for tag resolution and unknown-tag handling.

Integration changes outside streams/

  • api/src/tags/*resolveTag(), listStreamIdsForTag(), and TagsRepository export, all serving the new filter.
  • api/src/auth/auth.service.ts, auth.controller.ts — restored the intended SDK auth types drift from the API: expiresIn does not exist on the wire and refresh cannot authenticate #527/JWT revocation is ineffective: logout and password changes never invalidate issued tokens #510 design that a bad merge had clobbered: refresh() takes the token string (body-first, cookie fallback in the controller) and signs the isAdmin claim the guard reads.
  • api/src/auth/users.repository.ts — removed a stale duplicate findById (bad-merge artifact).
  • api/src/gateways/streams.gateway.ts — destructures { userId } from authenticate()'s { userId, isAdmin } result.
  • api/src/main.ts, streams.module.ts, audit.module.ts — removed duplicate imports/definitions that broke nest build.
  • Stale specs (auth.controller.spec, auth.service.spec, audit.interceptor.spec, audit.integration.spec, jwt-extractor.service.spec, streams.gateway.spec, users.service.spec, audit.integration.spec, database.integration.spec) — updated to the contracts those sources actually implement (logSafely, { userId, isAdmin }, is_admin on User, refresh token-string extraction), and fixed the keyset-pagination test's cursor to keep microsecond precision instead of truncating via toISOString().
  • xstreamroll-sdk/__tests__/client.test.ts — deleted; it tested the removed axios-based client (could not compile) and is fully superseded by the nock-based client.integration.test.ts.

Acceptance criteria coverage

  • GET /streams?q=<term> returns only streams whose name or description contains the term, case-insensitively (api/src/streams/repository/streams.repository.spec.ts, streams-db.repository.spec.ts, contract-provider.spec.tslist-streams-search)
  • GET /streams?tag=<slug|id> returns only streams carrying that tag (tags.service.spec.ts, streams.service.spec.ts, contract-provider.spec.tslist-streams-by-tag)
  • Unknown tags return an empty page rather than an error (streams.service.spec.ts)
  • LIKE wildcards in q are matched literally, never as wildcards (streams.repository.spec.ts, streams-db.repository.spec.ts)
  • Filters compose with existing pagination and visibility semantics (streams.repository.spec.ts)
  • SDK listStreams() sends the same filters the API contract expects (contract.consumer.test.ts)
  • App search box can pass q/tag through without client-side post-filtering (useStreams.test.tsx)

Test plan

  • cd api && npx jest482/482 passing (42/42 suites) against a fresh Postgres 16 test DB
  • cd api && npx tsc --noEmit — 0 errors
  • cd api && npx eslint "src/**/*.ts" — 0 errors (pre-existing warnings only, on files untouched by this PR)
  • cd api && npm run build — succeeds
  • cd xstreamroll-sdk && npx jest70/70 passing (6/6 suites)
  • cd xstreamroll-sdk && npm run typecheck — 0 errors; npm run lint — 0 errors
  • cd app && npx jest224/224 passing (19/20 suites); the one failing suite (fetch-json.test.ts) fails identically on main — a Node 24/jsdom environment incompatibility in a file this PR does not touch
  • cd app && npm run typecheck — 0 errors; npm run lint — 0 errors
  • npm run build --workspace=packages/types and --workspace=tests/contracts — succeed

Env vars / Notes

No new environment variables or migrations. q/tag are additive query params; existing GET /streams callers are unaffected. The q search is intentionally substring-based (not fuzzy); %/_ in user input are escaped to match literally. Tag filtering matches on the stream_tags association table, so a stream tagged with the given tag is returned regardless of whether the tag slug or numeric id was supplied.

Add server-side filtering to GET /streams (issue XStreamRollz#532): `q` does a
case-insensitive substring match over name and description with LIKE
wildcards escaped so user input is matched literally, and `tag`
filters by an existing tag slug or numeric id via the stream_tags
join. The tag value is resolved once in StreamsService through the
shared tag lookup (slug or id) and passed to the repository as a
concrete tagId, so neither the SQL nor the in-memory repository
re-implements slugification.

Wire the filters through the Next.js app (listStreams query params +
React Query keys) and the SDK (new StreamListParams and listStreams()
method), and extend the contract suite with list-streams-search and
list-streams-by-tag so the provider (api) and consumer (sdk) both
pin the new query surface.

Also repairs pre-existing breakage that blocked the quality gates:
bad-merge artifacts in auth (duplicate refresh implementations,
missing isAdmin claim), the socket gateway, streams module, audit
module, and main.ts, plus stale specs that no longer matched the
intended contracts (audit logSafely, gateway authenticate shape,
refresh token extraction). The SDK's stale axios-based client test
was removed; the nock-based integration suite supersedes it.

Closes XStreamRollz#532
api/package.json gained @types/cookie-parser@^1.4.10 (auth refresh
work) without a lockfile regeneration, so every `npm ci` in CI fails
with EUSAGE. Regenerate the root lockfile to add the missing
resolution, sync the stale api/sdk workspace version entries, and
prune an unreferenced nested conventional-commits-parser entry.

Copy link
Copy Markdown
Contributor Author

CI status update

New commit pushed: fix(ci): sync root package-lock.json so npm ci resolves dependencies

The CI quality matrix was blocked at the very first step (npm ci fails with EUSAGE — Missing: @types/[email protected] from lock file). api/package.json gained @types/cookie-parser@^1.4.10 in the auth-refresh work without a lockfile regeneration, so this failed on main and every PR. Regenerated the root lockfile (adds the missing resolution, syncs stale workspace version entries, prunes one unreferenced nested entry). Verified locally: npm ci clean, verify-lockfiles.sh passes.

Current check status:

  • CI quality matrix: awaiting maintainer approval — GitHub's fork-PR gate (action_required); the run executes once approved. All gates were validated locally: api 482/482 tests, sdk 70/70, app 224/224, typecheck/lint/build clean.
  • Trivy filesystem scan: passed.
  • Trivy image scans (api/app/processing): fail for pre-existing Dockerfile breakage on mainapi/pnpm-lock.yaml is out of sync with api/package.json (opentelemetry ^0.57.0 vs ^0.57.2), and the app/processing Dockerfiles cannot resolve workspace deps (@xstreamroll/types). These failures are identical on main and every other open PR; this PR touches no Dockerfiles. Happy to open a follow-up PR to repair the Docker build pipeline if that's wanted.

@Xhristin3
Xhristin3 merged commit 070a431 into XStreamRollz:main Aug 27, 2026
4 of 14 checks passed
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.

Feature: Stream Search And Tag Filtering

2 participants