Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
namespace PaperlessREST.Features.BatchProcessing.Presentation.Dto;
using System.Diagnostics.CodeAnalysis;
using System.Xml.Serialization;

namespace Paperless.Contracts.BatchProcessing;

[XmlRoot("accessReport")]
[ExcludeFromCodeCoverage(Justification = "DTO record - XML serialization tested via ReportProcessor tests")]
Expand Down
133 changes: 133 additions & 0 deletions Paperless.Contracts/DocumentManagement/DocumentDtos.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Paperless.Contracts.Validation;

namespace Paperless.Contracts.DocumentManagement;

/// <summary>
/// Query parameters for paginated document listing.
/// </summary>
/// <remarks>
/// Uses cursor-based pagination with GUIDv7 (time-ordered) document IDs.
/// Pass the last document's ID as <see cref="Cursor"/> to get the next page.
/// </remarks>
public sealed record PaginationQuery
{
[Range(PaginationConstraints.MinPageSize, PaginationConstraints.MaxPageSize,
ErrorMessage = "Page size must be between 1 and 100")]
[Description("Number of documents to return per page")]
public int PageSize { get; init; } = PaginationConstraints.DefaultPageSize;

[Description("Cursor for pagination (last document ID from previous page)")]
public Guid? Cursor { get; init; }
}

/// <summary>
/// Query parameters for document search.
/// </summary>
public sealed record SearchQuery
{
[StringLength(SearchConstraints.QueryMaxLength, MinimumLength = SearchConstraints.QueryMinLength,
ErrorMessage = "Search query must be between 1 and 100 characters")]
[Description("Search text to find in documents")]
public required string Query { get; init; }

[Range(1, SearchConstraints.MaxResultLimit, ErrorMessage = "Limit must be between 1 and 100")]
[Description("Maximum number of results to return")]
public int Limit { get; init; } = SearchConstraints.DefaultResultLimit;
}

/// <summary>
/// Represents document metadata returned by the API.
/// </summary>
public sealed record DocumentDto
{
[Description("Unique document identifier")]
public required Guid Id { get; init; }

[Description("Original PDF filename")] public required string FileName { get; init; }

[Description("Processing status (Pending, Completed, Failed)")]
public required string Status { get; init; }

[Description("Upload timestamp (UTC)")]
public required DateTimeOffset CreatedAt { get; init; }

[Description("Processing completion timestamp (UTC)")]
public DateTimeOffset? ProcessedAt { get; init; }

[Description("OCR extracted text content")]
public string? Content { get; init; }

[Description("AI-generated summary")] public string? Summary { get; init; }

[Description("Timestamp when summary was generated (UTC)")]
public DateTimeOffset? SummaryGeneratedAt { get; init; }
}

/// <summary>
/// Response returned after successfully uploading a document.
/// </summary>
public sealed record CreateDocumentResponse
{
[Description("Unique document identifier")]
public required Guid Id { get; init; }

[Description("Original PDF filename")] public required string FileName { get; init; }

[Description("Processing status")] public required string Status { get; init; }

[Description("Upload timestamp (UTC)")]
public required DateTimeOffset CreatedAt { get; init; }
}

/// <summary>
/// Represents a document search result with relevant fields.
/// </summary>
public sealed record DocumentSearchResultDto
{
[Description("Unique document identifier")]
public required Guid Id { get; init; }

[Description("Original PDF filename")] public required string FileName { get; init; }

[Description("OCR extracted content")] public string? Content { get; init; }

[Description("AI-generated summary")] public string? Summary { get; init; }

[Description("Upload timestamp (UTC)")]
public required DateTimeOffset CreatedAt { get; init; }

[Description("Processing status")] public required string Status { get; init; }
}

/// <summary>
/// Response containing a document summary.
/// </summary>
public sealed record SummaryDto
{
[Description("AI-generated summary of the document content")]
public string? Summary { get; init; }
}

/// <summary>
/// Paginated response wrapper for document listings.
/// </summary>
/// <remarks>
/// Uses cursor-based pagination for efficient traversal of large datasets.
/// The <see cref="NextCursor"/> is null when there are no more pages.
/// </remarks>
public sealed record PaginatedDocumentsResponse
{
[Description("List of documents in this page")]
public required List<DocumentDto> Items { get; init; }

[Description("Cursor for the next page (null if no more pages)")]
public Guid? NextCursor { get; init; }

[Description("Whether more documents exist after this page")]
public required bool HasMore { get; init; }

[Description("Number of documents in this response")]
public int Count => Items.Count;
}
11 changes: 11 additions & 0 deletions Paperless.Contracts/Paperless.Contracts.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>Paperless.Contracts</RootNamespace>
</PropertyGroup>

</Project>
34 changes: 34 additions & 0 deletions Paperless.Contracts/Validation/Constraints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace Paperless.Contracts.Validation;

/// <summary>
/// Search query validation constraints applied at the HTTP boundary.
/// </summary>
public static class SearchConstraints
{
/// <summary>Maximum query string length for DTO validation.</summary>
public const int QueryMaxLength = 100;

/// <summary>Minimum query string length.</summary>
public const int QueryMinLength = 1;

/// <summary>Maximum results that can be requested.</summary>
public const int MaxResultLimit = 100;

/// <summary>Default number of results returned.</summary>
public const int DefaultResultLimit = 10;
}

/// <summary>
/// Pagination constraints for document listing applied at the HTTP boundary.
/// </summary>
public static class PaginationConstraints
{
/// <summary>Default page size when not specified.</summary>
public const int DefaultPageSize = 20;

/// <summary>Maximum page size allowed.</summary>
public const int MaxPageSize = 100;

/// <summary>Minimum page size allowed.</summary>
public const int MinPageSize = 1;
}
1 change: 1 addition & 0 deletions Paperless.slnx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<Solution>
<Project Path="Paperless.Contracts/Paperless.Contracts.csproj" />
<Project Path="PaperlessREST/PaperlessREST.csproj" />
<Project Path="PaperlessServices/PaperlessServices.csproj" />
<Project Path="PaperlessREST.Tests/PaperlessREST.Tests.csproj" />
Expand Down
5 changes: 5 additions & 0 deletions PaperlessREST.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@
global using PaperlessREST.Features.DocumentManagement.Presentation.Dto;
global using PaperlessREST.Features.DocumentManagement.Presentation.Endpoints;

// Project namespaces - Paperless.Contracts (transport boundary)
global using Paperless.Contracts.BatchProcessing;
global using Paperless.Contracts.DocumentManagement;
global using Paperless.Contracts.Validation;

// ErrorOr for result pattern
global using ErrorOr;

Expand Down
1 change: 1 addition & 0 deletions PaperlessREST.Tests/PaperlessREST.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@

<ItemGroup>
<ProjectReference Include="..\PaperlessREST\PaperlessREST.csproj"/>
<ProjectReference Include="..\Paperless.Contracts\Paperless.Contracts.csproj"/>
</ItemGroup>


Expand Down
32 changes: 3 additions & 29 deletions PaperlessREST/Configuration/Constraints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,13 @@ public static class FileUploadConstraints
}

/// <summary>
/// Search query validation constraints.
/// Server-side search constants that don't belong on the public contract.
/// Boundary-level <see cref="Paperless.Contracts.Validation.SearchConstraints"/> covers query / limit ranges.
/// </summary>
public static class SearchConstraints
public static class SearchServiceConstraints
{
/// <summary>Maximum query string length for DTO validation.</summary>
public const int QueryMaxLength = 100;

/// <summary>Minimum query string length.</summary>
public const int QueryMinLength = 1;

/// <summary>Maximum query length at service layer (truncation threshold).</summary>
public const int ServiceQueryMaxLength = 1000;

/// <summary>Maximum results that can be requested.</summary>
public const int MaxResultLimit = 100;

/// <summary>Default number of results returned.</summary>
public const int DefaultResultLimit = 10;
}

/// <summary>
/// Pagination constraints for document listing.
/// </summary>
public static class PaginationConstraints
{
/// <summary>Default page size when not specified.</summary>
public const int DefaultPageSize = 20;

/// <summary>Maximum page size allowed.</summary>
public const int MaxPageSize = 100;

/// <summary>Minimum page size allowed.</summary>
public const int MinPageSize = 1;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ public async IAsyncEnumerable<T> SearchAsync<T>(string query, int limit = 10,
{
logger.LogInformation("Searching for query: {Query} (limit: {Limit})", query, limit);

string searchQuery = query.Length > SearchConstraints.ServiceQueryMaxLength
? query[..SearchConstraints.ServiceQueryMaxLength]
string searchQuery = query.Length > SearchServiceConstraints.ServiceQueryMaxLength
? query[..SearchServiceConstraints.ServiceQueryMaxLength]
: query;

SearchResponse<T> response = await elastic.SearchAsync<T>(
Expand Down
Loading