Skip to content

[Feature]: List artifacts by project endpoint #236

Description

@daniilperkin

User Story

As a logged-in SprintStart user,
I want to retrieve a paginated list of ingested artifacts scoped to a project I have access to,
so that I can browse and select knowledge-base artifacts without pulling their full content upfront.

Context & Motivation

The frontend knowledge-base feature (sprintstart-frontend/src/features/knowledge-base/) currently runs entirely on mocks (knowledgeService.ts:16-23 toggled by VITE_USE_MOCK_API). The first step to unmocking it is exposing a list endpoint the frontend already expects: GET /api/v1/projects/{projectId}/artifacts.

The backend's ArtifactController.kt:44 only exposes single-artifact content retrieval (getArtifactContent); there is no "list artifacts by project" route. The Artifact entity (ingestion/model/entity/Artifact.kt:17-53) stores the data, but ArtifactRepository only offers findBySourceId and findAllByIngestionRunId — neither is project-scoped.

Scope is intentionally minimal: add the list endpoint over the existing ingestion corpus. The frontend will be adapted to the backend's honest response shape (no invented freshness/canonical/tags/owner/aiSummary fields). No new module, no Flyway migration, no schema change, no new events.

Acceptance Criteria

Given an authenticated user with USER role, when they call GET /api/v1/projects/{projectId}/artifacts with a valid project UUID, then the endpoint returns 200 OK with a List JSON body ordered newest-first by ingestedAt.
Given an authenticated user with no access to the requested project, when they call the endpoint, then it returns 403 Forbidden.
Given a project with zero artifacts, when the endpoint is called, then it returns 200 OK with an empty list ([]).
Given the response, then each item exposes the entity's real fields (id, title, artifactType, sourceSystem, sourceId, sourceUrl, mime, language, ingestedAt, createdAtSource, updatedAtSource, contentHash, ingestionRunId) and excludes bodyText (heavy; fetched separately via the content endpoint).
Given an unauthenticated request, when the endpoint is called, then it returns 401 Unauthorized.
Given the endpoint is wired into the existing ArtifactController (@RequestMapping("/api/v1")), then it is annotated @PreAuthorize("hasRole('USER')") and reuses the existing UserApi.userHasAccessToProject auth check used by getArtifactContent.
Given ./gradlew build is run, then ktlint, detekt, and the new unit tests pass.
Given the artifact_projects join table, then the repository query correctly resolves the @ElementCollection (Artifact.kt:37-43) via JPQL :projectId member of a.projectIdsInternal.

Technical Notes

Affected components: sprintstart-backend/ (RESTRICTED — requires approval per AGENTS.md). No frontend changes in this issue; a follow-up issue will adapt the frontend Artifact type and knowledgeService.ts to the new response shape.
Files to add/edit:

  1. NEW src/main/kotlin/.../ingestion/model/dto/response/ArtifactResponse.kt — immutable data class:
    data class ArtifactResponse(
    val id: UUID,
    val title: String?,
    val artifactType: ArtifactType,
    val sourceSystem: SourceSystem,
    val sourceId: String,
    val sourceUrl: String?,
    val mime: String?,
    val language: String?,
    val ingestedAt: Instant,
    val createdAtSource: Instant?,
    val updatedAtSource: Instant?,
    val contentHash: String?,
    val ingestionRunId: UUID,
    )
    (bodyText deliberately excluded from the list response — content is fetched per-artifact via GET .../artifacts/{artifactId}/content.)
  2. EDIT src/main/kotlin/.../ingestion/repository/ArtifactRepository.kt — add:
    @query("select a from Artifact a where :projectId member of a.projectIdsInternal order by a.ingestedAt desc")
    fun findAllByProjectId(@Param("projectId") projectId: UUID): List
  3. EDIT src/main/kotlin/.../ingestion/service/ArtifactService.kt — add listArtifacts(projectId: UUID, authId: String): List mirroring the auth flow of getArtifactContent (ArtifactService.kt:38-51): userApi.userHasAccessToProject(authId, projectId) → 403 if false, else map repository results to ArtifactResponse. Annotate @transactional(readOnly = true).
  4. EDIT src/main/kotlin/.../ingestion/controller/ArtifactController.kt — add:
    @GetMapping("/projects/{projectId}/artifacts")
    @PreAuthorize("hasRole('USER')")
    fun listArtifacts(
    @PathVariable projectId: UUID,
    @AuthenticationPrincipal jwt: Jwt,
    ): ResponseEntity<List> =
    ResponseEntity.ok(artifactService.listArtifacts(projectId, jwt.subject))
  5. NEW/EDIT src/test/kotlin/.../ingestion/ — unit tests for ArtifactService.listArtifacts (mock UserApi + ArtifactRepository) and controller layer test, mirroring existing test conventions (H2 + mockk/springmockk).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions