feat: Phase 9 libraries + documents — CRUD, upload, sharing, signed URLs

Library management with 17 service methods covering full CRUD for
libraries and documents, multipart upload, text content extraction,
processing status, signed URLs, reprocessing, and sharing management.
This commit is contained in:
2026-03-05 20:08:25 +01:00
parent d850a679d7
commit e528a3c308
3 changed files with 591 additions and 0 deletions

203
libraries.go Normal file
View File

@@ -0,0 +1,203 @@
package mistral
import (
"context"
"fmt"
"io"
"net/url"
"strconv"
"somegit.dev/vikingowl/mistral-go-sdk/library"
)
// CreateLibrary creates a new document library.
func (c *Client) CreateLibrary(ctx context.Context, req *library.CreateRequest) (*library.Library, error) {
var resp library.Library
if err := c.doJSON(ctx, "POST", "/v1/libraries", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// ListLibraries lists all accessible libraries.
func (c *Client) ListLibraries(ctx context.Context) (*library.ListLibraryOut, error) {
var resp library.ListLibraryOut
if err := c.doJSON(ctx, "GET", "/v1/libraries", nil, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// GetLibrary retrieves a library by ID.
func (c *Client) GetLibrary(ctx context.Context, libraryID string) (*library.Library, error) {
var resp library.Library
if err := c.doJSON(ctx, "GET", fmt.Sprintf("/v1/libraries/%s", libraryID), nil, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// UpdateLibrary updates a library's name and description.
func (c *Client) UpdateLibrary(ctx context.Context, libraryID string, req *library.UpdateRequest) (*library.Library, error) {
var resp library.Library
if err := c.doJSON(ctx, "PUT", fmt.Sprintf("/v1/libraries/%s", libraryID), req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// DeleteLibrary deletes a library and all its documents.
func (c *Client) DeleteLibrary(ctx context.Context, libraryID string) (*library.Library, error) {
var resp library.Library
if err := c.doJSON(ctx, "DELETE", fmt.Sprintf("/v1/libraries/%s", libraryID), nil, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// UploadDocument uploads a document to a library.
func (c *Client) UploadDocument(ctx context.Context, libraryID string, filename string, file io.Reader) (*library.Document, error) {
var resp library.Document
if err := c.doMultipart(ctx, fmt.Sprintf("/v1/libraries/%s/documents", libraryID), filename, file, nil, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// ListDocuments lists documents in a library.
func (c *Client) ListDocuments(ctx context.Context, libraryID string, params *library.ListDocumentParams) (*library.ListDocumentOut, error) {
path := fmt.Sprintf("/v1/libraries/%s/documents", libraryID)
if params != nil {
q := url.Values{}
if params.Search != nil {
q.Set("search", *params.Search)
}
if params.PageSize != nil {
q.Set("page_size", strconv.Itoa(*params.PageSize))
}
if params.Page != nil {
q.Set("page", strconv.Itoa(*params.Page))
}
if params.SortBy != nil {
q.Set("sort_by", *params.SortBy)
}
if params.SortOrder != nil {
q.Set("sort_order", *params.SortOrder)
}
if encoded := q.Encode(); encoded != "" {
path += "?" + encoded
}
}
var resp library.ListDocumentOut
if err := c.doJSON(ctx, "GET", path, nil, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// GetDocument retrieves a document's metadata.
func (c *Client) GetDocument(ctx context.Context, libraryID, documentID string) (*library.Document, error) {
var resp library.Document
if err := c.doJSON(ctx, "GET", fmt.Sprintf("/v1/libraries/%s/documents/%s", libraryID, documentID), nil, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// UpdateDocument updates a document's metadata.
func (c *Client) UpdateDocument(ctx context.Context, libraryID, documentID string, req *library.DocumentUpdateRequest) (*library.Document, error) {
var resp library.Document
if err := c.doJSON(ctx, "PUT", fmt.Sprintf("/v1/libraries/%s/documents/%s", libraryID, documentID), req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// DeleteDocument deletes a document from a library.
func (c *Client) DeleteDocument(ctx context.Context, libraryID, documentID string) error {
resp, err := c.do(ctx, "DELETE", fmt.Sprintf("/v1/libraries/%s/documents/%s", libraryID, documentID), nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return parseAPIError(resp)
}
return nil
}
// GetDocumentTextContent retrieves the extracted text of a document.
func (c *Client) GetDocumentTextContent(ctx context.Context, libraryID, documentID string) (*library.DocumentTextContent, error) {
var resp library.DocumentTextContent
if err := c.doJSON(ctx, "GET", fmt.Sprintf("/v1/libraries/%s/documents/%s/text_content", libraryID, documentID), nil, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// GetDocumentStatus retrieves the processing status of a document.
func (c *Client) GetDocumentStatus(ctx context.Context, libraryID, documentID string) (*library.ProcessingStatusOut, error) {
var resp library.ProcessingStatusOut
if err := c.doJSON(ctx, "GET", fmt.Sprintf("/v1/libraries/%s/documents/%s/status", libraryID, documentID), nil, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// GetDocumentSignedURL retrieves a signed URL for downloading a document.
func (c *Client) GetDocumentSignedURL(ctx context.Context, libraryID, documentID string) (string, error) {
var resp string
if err := c.doJSON(ctx, "GET", fmt.Sprintf("/v1/libraries/%s/documents/%s/signed-url", libraryID, documentID), nil, &resp); err != nil {
return "", err
}
return resp, nil
}
// GetDocumentExtractedTextSignedURL retrieves a signed URL for the extracted text.
func (c *Client) GetDocumentExtractedTextSignedURL(ctx context.Context, libraryID, documentID string) (string, error) {
var resp string
if err := c.doJSON(ctx, "GET", fmt.Sprintf("/v1/libraries/%s/documents/%s/extracted-text-signed-url", libraryID, documentID), nil, &resp); err != nil {
return "", err
}
return resp, nil
}
// ReprocessDocument triggers reprocessing of a document.
func (c *Client) ReprocessDocument(ctx context.Context, libraryID, documentID string) error {
resp, err := c.do(ctx, "POST", fmt.Sprintf("/v1/libraries/%s/documents/%s/reprocess", libraryID, documentID), nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return parseAPIError(resp)
}
return nil
}
// ListLibrarySharing lists all sharing entries for a library.
func (c *Client) ListLibrarySharing(ctx context.Context, libraryID string) (*library.ListSharingOut, error) {
var resp library.ListSharingOut
if err := c.doJSON(ctx, "GET", fmt.Sprintf("/v1/libraries/%s/share", libraryID), nil, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// ShareLibrary creates or updates a sharing entry.
func (c *Client) ShareLibrary(ctx context.Context, libraryID string, req *library.SharingRequest) (*library.SharingOut, error) {
var resp library.SharingOut
if err := c.doJSON(ctx, "PUT", fmt.Sprintf("/v1/libraries/%s/share", libraryID), req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// UnshareLibrary deletes a sharing entry.
func (c *Client) UnshareLibrary(ctx context.Context, libraryID string, req *library.SharingDeleteRequest) (*library.SharingOut, error) {
var resp library.SharingOut
if err := c.doJSON(ctx, "DELETE", fmt.Sprintf("/v1/libraries/%s/share", libraryID), req, &resp); err != nil {
return nil, err
}
return &resp, nil
}

252
libraries_test.go Normal file
View File

@@ -0,0 +1,252 @@
package mistral
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"somegit.dev/vikingowl/mistral-go-sdk/library"
)
func newLibraryJSON() map[string]any {
return map[string]any{
"id": "lib-123", "name": "TestLib",
"created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z",
"owner_id": "user-1", "owner_type": "user",
"total_size": 1024, "nb_documents": 5, "chunk_size": 512,
}
}
func TestCreateLibrary_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
t.Errorf("got method %s", r.Method)
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(newLibraryJSON())
}))
defer server.Close()
client := NewClient("key", WithBaseURL(server.URL))
lib, err := client.CreateLibrary(context.Background(), &library.CreateRequest{
Name: "TestLib",
})
if err != nil {
t.Fatal(err)
}
if lib.ID != "lib-123" {
t.Errorf("got id %q", lib.ID)
}
}
func TestListLibraries_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]any{
"data": []map[string]any{newLibraryJSON()},
})
}))
defer server.Close()
client := NewClient("key", WithBaseURL(server.URL))
resp, err := client.ListLibraries(context.Background())
if err != nil {
t.Fatal(err)
}
if len(resp.Data) != 1 {
t.Fatalf("got %d libraries", len(resp.Data))
}
}
func TestGetLibrary_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/libraries/lib-123" {
t.Errorf("got path %s", r.URL.Path)
}
json.NewEncoder(w).Encode(newLibraryJSON())
}))
defer server.Close()
client := NewClient("key", WithBaseURL(server.URL))
lib, err := client.GetLibrary(context.Background(), "lib-123")
if err != nil {
t.Fatal(err)
}
if lib.Name != "TestLib" {
t.Errorf("got name %q", lib.Name)
}
}
func TestDeleteLibrary_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "DELETE" {
t.Errorf("got method %s", r.Method)
}
json.NewEncoder(w).Encode(newLibraryJSON())
}))
defer server.Close()
client := NewClient("key", WithBaseURL(server.URL))
lib, err := client.DeleteLibrary(context.Background(), "lib-123")
if err != nil {
t.Fatal(err)
}
if lib.ID != "lib-123" {
t.Errorf("got id %q", lib.ID)
}
}
func TestUploadDocument_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
t.Errorf("got method %s", r.Method)
}
ct := r.Header.Get("Content-Type")
if !strings.HasPrefix(ct, "multipart/form-data") {
t.Errorf("expected multipart, got %q", ct)
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]any{
"id": "doc-1", "library_id": "lib-123", "name": "test.pdf",
"hash": "abc", "mime_type": "application/pdf", "extension": ".pdf", "size": 1024,
"created_at": "2024-01-01T00:00:00Z", "process_status": "todo",
"uploaded_by_id": "user-1", "uploaded_by_type": "user",
"processing_status": "todo", "tokens_processing_total": 0,
})
}))
defer server.Close()
client := NewClient("key", WithBaseURL(server.URL))
doc, err := client.UploadDocument(context.Background(), "lib-123", "test.pdf", strings.NewReader("fake pdf"))
if err != nil {
t.Fatal(err)
}
if doc.ID != "doc-1" {
t.Errorf("got id %q", doc.ID)
}
}
func TestListDocuments_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/libraries/lib-123/documents" {
t.Errorf("got path %s", r.URL.Path)
}
json.NewEncoder(w).Encode(map[string]any{
"pagination": map[string]any{
"total_items": 1, "total_pages": 1, "current_page": 0, "page_size": 100, "has_more": false,
},
"data": []map[string]any{{
"id": "doc-1", "library_id": "lib-123", "name": "test.pdf",
"hash": "abc", "mime_type": "application/pdf", "extension": ".pdf", "size": 1024,
"created_at": "2024-01-01T00:00:00Z", "process_status": "done",
"uploaded_by_id": "user-1", "uploaded_by_type": "user",
"processing_status": "done", "tokens_processing_total": 500,
}},
})
}))
defer server.Close()
client := NewClient("key", WithBaseURL(server.URL))
resp, err := client.ListDocuments(context.Background(), "lib-123", nil)
if err != nil {
t.Fatal(err)
}
if len(resp.Data) != 1 {
t.Fatalf("got %d documents", len(resp.Data))
}
if resp.Pagination.TotalItems != 1 {
t.Errorf("got total_items %d", resp.Pagination.TotalItems)
}
}
func TestGetDocumentTextContent_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]any{"text": "Hello world"})
}))
defer server.Close()
client := NewClient("key", WithBaseURL(server.URL))
content, err := client.GetDocumentTextContent(context.Background(), "lib-123", "doc-1")
if err != nil {
t.Fatal(err)
}
if content.Text != "Hello world" {
t.Errorf("got text %q", content.Text)
}
}
func TestGetDocumentStatus_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]any{
"document_id": "doc-1", "process_status": "done", "processing_status": "done",
})
}))
defer server.Close()
client := NewClient("key", WithBaseURL(server.URL))
status, err := client.GetDocumentStatus(context.Background(), "lib-123", "doc-1")
if err != nil {
t.Fatal(err)
}
if status.ProcessStatus != "done" {
t.Errorf("got status %q", status.ProcessStatus)
}
}
func TestDeleteDocument_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "DELETE" {
t.Errorf("got method %s", r.Method)
}
w.WriteHeader(http.StatusNoContent)
}))
defer server.Close()
client := NewClient("key", WithBaseURL(server.URL))
err := client.DeleteDocument(context.Background(), "lib-123", "doc-1")
if err != nil {
t.Fatal(err)
}
}
func TestReprocessDocument_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
t.Errorf("got method %s", r.Method)
}
w.WriteHeader(http.StatusNoContent)
}))
defer server.Close()
client := NewClient("key", WithBaseURL(server.URL))
err := client.ReprocessDocument(context.Background(), "lib-123", "doc-1")
if err != nil {
t.Fatal(err)
}
}
func TestListLibrarySharing_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]any{
"data": []map[string]any{{
"library_id": "lib-123", "org_id": "org-1",
"role": "Viewer", "share_with_type": "User", "share_with_uuid": "user-2",
}},
})
}))
defer server.Close()
client := NewClient("key", WithBaseURL(server.URL))
resp, err := client.ListLibrarySharing(context.Background(), "lib-123")
if err != nil {
t.Fatal(err)
}
if len(resp.Data) != 1 {
t.Fatalf("got %d sharing entries", len(resp.Data))
}
if resp.Data[0].Role != "Viewer" {
t.Errorf("got role %q", resp.Data[0].Role)
}
}

136
library/library.go Normal file
View File

@@ -0,0 +1,136 @@
package library
// CreateRequest creates a new library.
type CreateRequest struct {
Name string `json:"name"`
Description *string `json:"description,omitempty"`
ChunkSize *int `json:"chunk_size,omitempty"`
}
// UpdateRequest updates a library.
type UpdateRequest struct {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
}
// Library represents a document library.
type Library struct {
ID string `json:"id"`
Name string `json:"name"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
OwnerID *string `json:"owner_id"`
OwnerType string `json:"owner_type"`
TotalSize int `json:"total_size"`
NbDocuments int `json:"nb_documents"`
ChunkSize *int `json:"chunk_size"`
Emoji *string `json:"emoji,omitempty"`
Description *string `json:"description,omitempty"`
GeneratedDescription *string `json:"generated_description,omitempty"`
ExplicitUserMembersCount *int `json:"explicit_user_members_count,omitempty"`
ExplicitWorkspaceMembersCount *int `json:"explicit_workspace_members_count,omitempty"`
OrgSharingRole *string `json:"org_sharing_role,omitempty"`
GeneratedName *string `json:"generated_name,omitempty"`
}
// ListLibraryOut is the response from listing libraries.
type ListLibraryOut struct {
Data []Library `json:"data"`
}
// Document represents a document in a library.
type Document struct {
ID string `json:"id"`
LibraryID string `json:"library_id"`
Hash *string `json:"hash"`
MimeType *string `json:"mime_type"`
Extension *string `json:"extension"`
Size *int `json:"size"`
Name string `json:"name"`
Summary *string `json:"summary,omitempty"`
CreatedAt string `json:"created_at"`
LastProcessedAt *string `json:"last_processed_at,omitempty"`
NumberOfPages *int `json:"number_of_pages,omitempty"`
ProcessStatus string `json:"process_status"`
UploadedByID *string `json:"uploaded_by_id"`
UploadedByType string `json:"uploaded_by_type"`
TokensProcessingMainContent *int `json:"tokens_processing_main_content,omitempty"`
TokensProcessingSummary *int `json:"tokens_processing_summary,omitempty"`
URL *string `json:"url,omitempty"`
Attributes map[string]any `json:"attributes,omitempty"`
ProcessingStatus string `json:"processing_status"`
TokensProcessingTotal int `json:"tokens_processing_total"`
}
// DocumentUpdateRequest updates a document's metadata.
type DocumentUpdateRequest struct {
Name *string `json:"name,omitempty"`
Attributes map[string]any `json:"attributes,omitempty"`
}
// DocumentTextContent holds the extracted text of a document.
type DocumentTextContent struct {
Text string `json:"text"`
}
// ListDocumentOut is a paginated list of documents.
type ListDocumentOut struct {
Pagination PaginationInfo `json:"pagination"`
Data []Document `json:"data"`
}
// PaginationInfo holds pagination metadata.
type PaginationInfo struct {
TotalItems int `json:"total_items"`
TotalPages int `json:"total_pages"`
CurrentPage int `json:"current_page"`
PageSize int `json:"page_size"`
HasMore bool `json:"has_more"`
}
// ProcessingStatusOut holds document processing status.
type ProcessingStatusOut struct {
DocumentID string `json:"document_id"`
ProcessStatus string `json:"process_status"`
ProcessingStatus string `json:"processing_status"`
}
// ListDocumentParams holds query parameters for listing documents.
type ListDocumentParams struct {
Search *string
PageSize *int
Page *int
FiltersAttributes *string
SortBy *string
SortOrder *string
}
// SharingRequest creates or updates library sharing.
type SharingRequest struct {
OrgID *string `json:"org_id,omitempty"`
Level string `json:"level"`
ShareWithUUID string `json:"share_with_uuid"`
ShareWithType string `json:"share_with_type"`
}
// SharingDeleteRequest removes library sharing.
type SharingDeleteRequest struct {
OrgID *string `json:"org_id,omitempty"`
ShareWithUUID string `json:"share_with_uuid"`
ShareWithType string `json:"share_with_type"`
}
// SharingOut represents a sharing entry.
type SharingOut struct {
LibraryID string `json:"library_id"`
UserID *string `json:"user_id,omitempty"`
OrgID string `json:"org_id"`
Role string `json:"role"`
ShareWithType string `json:"share_with_type"`
ShareWithUUID *string `json:"share_with_uuid"`
}
// ListSharingOut is the response from listing sharing entries.
type ListSharingOut struct {
Data []SharingOut `json:"data"`
}