Skip to content

feat: search files by name - #47

Merged
hsc00 merged 4 commits into
mainfrom
us-10-search-files-by-name
May 6, 2026
Merged

hsc00 merged 4 commits into
mainfrom
us-10-search-files-by-name

Conversation

@hsc00

@hsc00 hsc00 commented May 6, 2026 •

Copy link
Copy Markdown
Owner

Summary

This PR implements US-10: Search Files by Name, adding a GraphQL searchAssets query for account-scoped file-name search across uploaded digital assets. The feature enables users to search their uploaded files with pagination support, friendly error handling, and deterministic result ordering.

Core Changes

Application Layer

  • SearchAssetsQuery — immutable command DTO capturing account, query string, page, and pageSize
  • SearchAssetsService — orchestrates trimmed query validation, pagination calculation, repository calls, and error translation
  • Result Types — SearchAssetsResult, SearchAssetsFile, SearchAssetsPageInfo encapsulate the domain response for resolver mapping

Domain Contracts (ADR-04)

Extended AssetRepositoryInterface with:

  • searchByFileName(AccountId, string, AssetStatus, int, int): array — account-scoped, status-filtered, paginated file-name search with deterministic ordering (created_at DESC, id ASC)
  • countByFileName(AccountId, string, AssetStatus): int — count matching assets for pagination metadata without filtering in PHP

Infrastructure (MySQL)

  • Implemented trimmed literal LIKE search with proper escaping of % and _ characters
  • Added case-insensitive COLLATE matching and pagination via offset/limit
  • Enforces uploaded-only filtering at the database layer

GraphQL Integration

  • SearchAssetsResolver — maps GraphQL arguments to application query and transforms domain result into JSON response
  • Schema — added searchAssets(query: String!, page: Int = 1, pageSize: Int = 10) query with SearchAssetsPayload, SearchAssetsFile, and SearchAssetsPageInfo types
  • SchemaFactory — registered resolver into query root
  • Runtime — wired SearchAssetsService and SearchAssetsResolver into public/index.php bootstrap

Behavior

Query:

query {
  searchAssets(query: "invoice", page: 1, pageSize: 10) {
    files { id, fileName, mimeType, status }
    totalCount
    pageInfo { page, pageSize, totalPages }
    userErrors { code, message, field }
  }
}

Key Features:

  • Searches only UPLOADED assets scoped to authenticated account
  • Trims input query; returns friendly error if empty after trim
  • Page size capped at 100; defaults to 10
  • Empty results return empty files array with totalCount: 0
  • Repository failures translated to RepositoryUnavailableException

Testing

  • Unit (SearchAssetsServiceTest) — 292 lines covering empty-query rejection, pagination, page-size capping, offset calculation, and repository-failure translation
  • Unit (GraphQLHandlerTest) — 417 lines extended with search resolver wiring, GraphQL contract validation, and in-memory repository pagination implementation
  • Integration (MySQLAssetRepositoryTest) — 100 lines added for account scoping, uploaded-only filtering, pagination, empty-query handling, and literal %/_ search semantics
  • All validations passed: phpstan, 29 unit tests + 217 assertions, 14 integration tests

Documentation

  • Implementation Log (docs/logs/10-search-files-by-name.md) — detailed delivery scope, validation steps, and files changed
  • ADR-04 Update — formalized repository contract boundaries, pagination semantics, and status-filtering requirements
  • mkdocs.yml — navigation entry added for feature log

Architecture Alignment

Follows clean architecture (Domain → Application → Infrastructure → GraphQL) and DDD principles:

  • Trimmed query validation owned by application layer
  • Repository contract enforces pagination and status scoping at persistence boundary
  • GraphQL resolver remains a thin adapter
  • Error translation at application boundary prevents internal exceptions leaking to clients

@coderabbitai

coderabbitai Bot commented May 6, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@hsc00 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 35 minutes and 43 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 25e76c0c-08f9-48bc-8342-8c53b59f60dd

📥 Commits

Reviewing files that changed from the base of the PR and between d5f36b9 and da783cd.

📒 Files selected for processing (4)
  • docs/logs/10-search-files-by-name.md
  • src/Application/Asset/SearchAssetsService.php
  • src/GraphQL/Schema/schema.graphql
  • tests/Unit/Http/GraphQLHandlerTest.php
📝 Walkthrough

Walkthrough

This PR introduces the US-10 feature to search assets by file name. It adds a new GraphQL query endpoint with pagination, implements account-scoped, status-filtered file searches via repository methods, applies LIKE-based literal matching with proper escaping, and provides comprehensive unit and integration tests alongside updated ADR documentation.

Changes

Search Assets by File Name

Layer / File(s) Summary
Data Models & Commands
src/Application/Asset/Command/SearchAssetsQuery.php, src/Application/Asset/Result/SearchAssetsFile.php, src/Application/Asset/Result/SearchAssetsPageInfo.php, src/Application/Asset/Result/SearchAssetsResult.php
New immutable value objects model search input (accountId, query, page, pageSize) and results (files, totalCount, pageInfo, userErrors) with pagination metadata and total page computation.
Domain Contract
src/Domain/Asset/AssetRepositoryInterface.php
searchByFileName signature expanded to accept AssetStatus, offset, and limit for status-scoped, paginated searching; new countByFileName method added for total-count computation.
Infrastructure Implementation
src/Infrastructure/Persistence/MySQLAssetRepository.php
Implements paginated, status-filtered LIKE searches with deterministic ordering, escape handling via likeSearchQuery helper, and count queries for pagination metadata.
Application Service
src/Application/Asset/SearchAssetsService.php
Validates input (empty-query detection), computes pagination offsets, calls repository with proper constraints, wraps repository exceptions, and maps domain assets to result DTOs.
GraphQL API Layer
src/GraphQL/Schema/schema.graphql, src/GraphQL/Resolver/SearchAssetsResolver.php, src/GraphQL/SchemaFactory.php
New schema types (SearchAssetsFile, SearchAssetsPageInfo, SearchAssetsPayload) and searchAssets query field added; resolver wires service into GraphQL execution and transforms domain results to schema-compliant arrays.
Runtime Wiring
public/index.php
Instantiates SearchAssetsService and SearchAssetsResolver, injects resolver into SchemaFactory for schema decoration.
Documentation
docs/adr/04-asset-domain-contracts.md, docs/logs/10-search-files-by-name.md, mkdocs.yml
ADR-04 documents expanded repository and storage contracts; new implementation log details feature design, components, and validation steps; mkdocs navigation updated.
Tests
tests/Integration/Infrastructure/Persistence/MySQLAssetRepositoryTest.php, tests/Unit/Application/Asset/SearchAssetsServiceTest.php, tests/Unit/Http/GraphQLHandlerTest.php
Integration tests verify deterministic ordering, literal escaping, and trimming behavior; unit tests validate service pagination, error handling, and result mapping; GraphQL handler tests exercise end-to-end query execution.

Sequence Diagram

sequenceDiagram
    actor Client
    participant GraphQL as GraphQL Handler
    participant Resolver as SearchAssetsResolver
    participant Service as SearchAssetsService
    participant Repo as AssetRepository
    participant DB as MySQL Database

    Client->>GraphQL: searchAssets(query, page, pageSize)
    GraphQL->>Resolver: resolve(args, context)
    Resolver->>Resolver: Extract accountId from context
    Resolver->>Service: searchAssets(SearchAssetsQuery)
    Service->>Service: Validate & normalize input
    Service->>Repo: countByFileName(accountId, query, status)
    Repo->>DB: SELECT COUNT(*) WHERE account_id=? AND status=? AND file_name LIKE ?
    DB-->>Repo: totalCount
    Service->>Repo: searchByFileName(accountId, query, status, offset, limit)
    Repo->>DB: SELECT * WHERE account_id=? AND status=? AND file_name LIKE ? ORDER BY created_at, id LIMIT ? OFFSET ?
    DB-->>Repo: Asset rows
    Repo-->>Service: Asset objects
    Service->>Service: Map Assets to SearchAssetsFile DTOs
    Service-->>Resolver: SearchAssetsResult
    Resolver->>Resolver: Transform to GraphQL response array
    Resolver-->>GraphQL: { files, totalCount, pageInfo, userErrors }
    GraphQL-->>Client: GraphQL response (JSON)
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Possibly Related PRs

  • hsc00/dam-api#36: Touches StorageAdapterInterface and UploadTarget type definitions referenced in updated ADR-04.
  • hsc00/dam-api#30: Modifies Asset domain surface including AssetStatus and UploadTarget types affected by domain contract changes.
  • hsc00/dam-api#44: Implements services depending on expanded AssetRepositoryInterface methods like findById and save.

Suggested Labels

enhancement

Poem

🐰 A search by name now bounces through the air,
With pages trimmed and status held with care,
The LIKE patterns dance, no percent escapes,
From GraphQL queries to MySQL's shapes.
Swift sorting, pagination, all in place,
The asset garden blooms at query's pace! 🌻

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is entirely missing, with no content provided in any of the required sections (Summary, Changes, Testing, Reviewer Notes). Provide a complete PR description following the template: add a summary, list changed files, document tests added, and note any important implementation details or limitations.
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature being implemented: search files by name.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch us-10-search-files-by-name

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/Integration/Infrastructure/Persistence/MySQLAssetRepositoryTest.php (1)

316-333: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Make the cross-account fixture eligible for the same status filter.

$otherAccountMatch is a pending asset, so this test still passes if account_id filtering is accidentally removed because the UPLOADED predicate excludes it first. Using an uploaded asset here would make the test actually guard against cross-account leakage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Integration/Infrastructure/Persistence/MySQLAssetRepositoryTest.php`
around lines 316 - 333, The cross-account fixture $otherAccountMatch is created
as a pending asset which doesn't trigger the UPLOADED filter; update the fixture
so $otherAccountMatch is created as an uploaded asset (use the same helper that
builds fixtures - e.g., change the call to pendingAsset to produce an uploaded
asset or use an uploadedAsset helper) so its status equals AssetStatus::UPLOADED
before repository->save; this ensures repository->countByFileName and
repository->searchByFileName (the tested methods) will actually verify
cross-account filtering.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/logs/10-search-files-by-name.md`:
- Around line 45-48: Clarify that the reported test counts refer to different
suites: update the text around the entries referencing
SearchAssetsServiceTest.php and GraphQLHandlerTest.php to explicitly label those
results as "unit tests" (29 tests, 217 assertions) and the MySQL run as
"integration tests" (OK 14 tests, 217 assertions), and add a brief note
explaining why the assertion count is the same across both runs (e.g.,
overlapping assertions or different subsets executed) so readers aren’t confused
by the identical assertion total.
- Line 13: Replace the current status line "**PR:** Creation in progress." with
a clear reference to the existing pull request by updating it to something like
"**PR:** Included in PR `#47`" (keeping the bold format); locate the exact string
"**PR:** Creation in progress." in docs/logs/10-search-files-by-name.md and
change it to reflect PR `#47` so the documentation accurately references the PR.

In `@src/Application/Asset/Result/SearchAssetsResult.php`:
- Around line 9-12: The docblock for the SearchAssetsResult constructor/type
hints references UserError but lacks a use/import, causing static analysers to
resolve it to the current namespace; locate the actual UserError class FQCN
(search for the class definition) and add a corresponding use statement (e.g.
use App\Path\To\UserError;) at the top of SearchAssetsResult.php so the `@param`
list<UserError> $userErrors docblock refers to the correct type; ensure the file
also imports SearchAssetsFile if not already present.

In `@src/Application/Asset/SearchAssetsService.php`:
- Around line 46-56: When totalCount returned by $this->assets->countByFileName
is 0, avoid calling $this->assets->searchByFileName and return the empty result
early: after computing $totalCount and $pageInfo in SearchAssetsService, add a
conditional that if $totalCount === 0 you construct and return the response
(using SearchAssetsPageInfo::fromTotalCount and an empty assets array) instead
of calling searchByFileName; this removes the unnecessary DB round-trip while
preserving existing pagination info.

In `@src/GraphQL/Schema/schema.graphql`:
- Around line 323-328: The GraphQL doc for SearchAssetsPageInfo::pageSize is
misleading—it's not the raw client-requested value but the effective,
validated/clamped size (capped by MAX_PAGE_SIZE). Update the pageSize field
description in the schema to state that it is the effective/validated page size
returned after server-side validation/clamping (mentioning MAX_PAGE_SIZE) so
clients understand the value may differ from their request.

In `@tests/Unit/Http/GraphQLHandlerTest.php`:
- Around line 962-967: The opening brace for the method createHandler should be
moved up to the same line as the function signature to satisfy PSR-12 (change
the current multi-line signature ending with "): array" followed by a newline
and "{" to a single-line signature with the brace). Edit the createHandler
method declaration so the opening brace is on the same line as the signature
(affects the function named createHandler and its parameter list), then run PHP
CS Fixer or your linter to confirm the signature formatting passes CI.

---

Outside diff comments:
In `@tests/Integration/Infrastructure/Persistence/MySQLAssetRepositoryTest.php`:
- Around line 316-333: The cross-account fixture $otherAccountMatch is created
as a pending asset which doesn't trigger the UPLOADED filter; update the fixture
so $otherAccountMatch is created as an uploaded asset (use the same helper that
builds fixtures - e.g., change the call to pendingAsset to produce an uploaded
asset or use an uploadedAsset helper) so its status equals AssetStatus::UPLOADED
before repository->save; this ensures repository->countByFileName and
repository->searchByFileName (the tested methods) will actually verify
cross-account filtering.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a9f148dc-8449-4f6f-8165-7e2c30b4b4fb

📥 Commits

Reviewing files that changed from the base of the PR and between efd5a54 and d5f36b9.

📒 Files selected for processing (17)
  • docs/adr/04-asset-domain-contracts.md
  • docs/logs/10-search-files-by-name.md
  • mkdocs.yml
  • public/index.php
  • src/Application/Asset/Command/SearchAssetsQuery.php
  • src/Application/Asset/Result/SearchAssetsFile.php
  • src/Application/Asset/Result/SearchAssetsPageInfo.php
  • src/Application/Asset/Result/SearchAssetsResult.php
  • src/Application/Asset/SearchAssetsService.php
  • src/Domain/Asset/AssetRepositoryInterface.php
  • src/GraphQL/Resolver/SearchAssetsResolver.php
  • src/GraphQL/Schema/schema.graphql
  • src/GraphQL/SchemaFactory.php
  • src/Infrastructure/Persistence/MySQLAssetRepository.php
  • tests/Integration/Infrastructure/Persistence/MySQLAssetRepositoryTest.php
  • tests/Unit/Application/Asset/SearchAssetsServiceTest.php
  • tests/Unit/Http/GraphQLHandlerTest.php

Comment thread docs/logs/10-search-files-by-name.md Outdated
Comment thread docs/logs/10-search-files-by-name.md
Comment thread src/Application/Asset/Result/SearchAssetsResult.php
Comment thread src/Application/Asset/SearchAssetsService.php
Comment thread src/GraphQL/Schema/schema.graphql
Comment thread tests/Unit/Http/GraphQLHandlerTest.php Outdated
@hsc00
hsc00 merged commit 9d0adcf into main May 6, 2026
8 checks passed
@hsc00
hsc00 deleted the us-10-search-files-by-name branch May 6, 2026 21:00
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