Merged in feature/add-deletes (pull request #214)
support delete for client, document and folder * support delete for client, document and folder * remove batch cancel conflict not used Approved-by: Jacob Mathison
This commit is contained in:
+421
-300
@@ -534,6 +534,18 @@ type CollectorSet struct {
|
||||
MinimumCleanerVersion *CodeVersion `json:"minimum_cleaner_version,omitempty"`
|
||||
}
|
||||
|
||||
// DeleteResponse Response for hard-delete operations when verbose=true
|
||||
type DeleteResponse struct {
|
||||
// DeletedDocuments S3 paths of orphaned source documents (only populated when verbose=true)
|
||||
DeletedDocuments []struct {
|
||||
// DocumentId ID of the deleted document
|
||||
DocumentId openapi_types.UUID `json:"documentId"`
|
||||
|
||||
// S3Path Full S3 path (s3://bucket/key) of the orphaned source file
|
||||
S3Path string `json:"s3Path"`
|
||||
} `json:"deletedDocuments"`
|
||||
}
|
||||
|
||||
// DocClient The properties of a client.
|
||||
type DocClient struct {
|
||||
// CanSync If the client is allowing active syncs
|
||||
@@ -1293,6 +1305,15 @@ type DeleteAdminUserParams struct {
|
||||
Confirm bool `form:"confirm" json:"confirm"`
|
||||
}
|
||||
|
||||
// DeleteClientParams defines parameters for DeleteClient.
|
||||
type DeleteClientParams struct {
|
||||
// Confirm Must be set to true to confirm deletion
|
||||
Confirm bool `form:"confirm" json:"confirm"`
|
||||
|
||||
// Verbose When true, returns S3 paths of orphaned source documents in the response body
|
||||
Verbose *bool `form:"verbose,omitempty" json:"verbose,omitempty"`
|
||||
}
|
||||
|
||||
// UploadDocumentMultipartBody defines parameters for UploadDocument.
|
||||
type UploadDocumentMultipartBody struct {
|
||||
// File The file to upload
|
||||
@@ -1324,6 +1345,12 @@ type ListClientFoldersParams struct {
|
||||
Metrics *bool `form:"metrics,omitempty" json:"metrics,omitempty"`
|
||||
}
|
||||
|
||||
// DeleteDocumentParams defines parameters for DeleteDocument.
|
||||
type DeleteDocumentParams struct {
|
||||
// Verbose When true, returns S3 paths of orphaned source documents in the response body
|
||||
Verbose *bool `form:"verbose,omitempty" json:"verbose,omitempty"`
|
||||
}
|
||||
|
||||
// GetDocumentParams defines parameters for GetDocument.
|
||||
type GetDocumentParams struct {
|
||||
// TextRecord When true, includes the full text extraction record in the response
|
||||
@@ -1351,6 +1378,15 @@ type GetFieldExtractionByVersionParams struct {
|
||||
Version int64 `form:"version" json:"version"`
|
||||
}
|
||||
|
||||
// DeleteFolderParams defines parameters for DeleteFolder.
|
||||
type DeleteFolderParams struct {
|
||||
// IncludeDocuments When true, also deletes all documents in the folder tree
|
||||
IncludeDocuments *bool `form:"include_documents,omitempty" json:"include_documents,omitempty"`
|
||||
|
||||
// Verbose When true, returns S3 paths of orphaned source documents in the response body
|
||||
Verbose *bool `form:"verbose,omitempty" json:"verbose,omitempty"`
|
||||
}
|
||||
|
||||
// GetFolderDocumentsParams defines parameters for GetFolderDocuments.
|
||||
type GetFolderDocumentsParams struct {
|
||||
// TextRecord When true, includes the full text extraction record for each document
|
||||
@@ -1464,6 +1500,9 @@ type ServerInterface interface {
|
||||
// Create a new client
|
||||
// (POST /client)
|
||||
CreateClient(ctx echo.Context) error
|
||||
// Hard-delete a client and all its data
|
||||
// (DELETE /client/{id})
|
||||
DeleteClient(ctx echo.Context, id ClientID, params DeleteClientParams) error
|
||||
// Get a client by ID
|
||||
// (GET /client/{id})
|
||||
GetClient(ctx echo.Context, id ClientID) error
|
||||
@@ -1488,9 +1527,6 @@ type ServerInterface interface {
|
||||
// Upload multiple PDF documents as ZIP archive
|
||||
// (POST /client/{id}/document/batch)
|
||||
UploadDocumentBatch(ctx echo.Context, id ClientID) error
|
||||
// Cancel a batch upload
|
||||
// (DELETE /client/{id}/document/batch/{batch_id})
|
||||
CancelDocumentBatch(ctx echo.Context, id ClientID, batchId BatchID) error
|
||||
// Get batch upload status
|
||||
// (GET /client/{id}/document/batch/{batch_id})
|
||||
GetDocumentBatch(ctx echo.Context, id ClientID, batchId BatchID) error
|
||||
@@ -1506,6 +1542,9 @@ type ServerInterface interface {
|
||||
// List all clients
|
||||
// (GET /clients)
|
||||
ListClients(ctx echo.Context) error
|
||||
// Hard-delete a document
|
||||
// (DELETE /document/{id})
|
||||
DeleteDocument(ctx echo.Context, id DocumentID, params DeleteDocumentParams) error
|
||||
// Get document details by its id
|
||||
// (GET /document/{id})
|
||||
GetDocument(ctx echo.Context, id DocumentID, params GetDocumentParams) error
|
||||
@@ -1542,6 +1581,9 @@ type ServerInterface interface {
|
||||
// Create a new folder
|
||||
// (POST /folders)
|
||||
CreateFolder(ctx echo.Context) error
|
||||
// Hard-delete a folder tree
|
||||
// (DELETE /folders/{folderId})
|
||||
DeleteFolder(ctx echo.Context, folderId openapi_types.UUID, params DeleteFolderParams) error
|
||||
// Rename a folder
|
||||
// (PATCH /folders/{folderId})
|
||||
RenameFolder(ctx echo.Context, folderId openapi_types.UUID) error
|
||||
@@ -1944,6 +1986,40 @@ func (w *ServerInterfaceWrapper) CreateClient(ctx echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteClient converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) DeleteClient(ctx echo.Context) error {
|
||||
var err error
|
||||
// ------------- Path parameter "id" -------------
|
||||
var id ClientID
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "id", ctx.Param("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err))
|
||||
}
|
||||
|
||||
ctx.Set(JwtAuthScopes, []string{})
|
||||
|
||||
// Parameter object where we will unmarshal all parameters from the context
|
||||
var params DeleteClientParams
|
||||
// ------------- Required query parameter "confirm" -------------
|
||||
|
||||
err = runtime.BindQueryParameter("form", true, true, "confirm", ctx.QueryParams(), ¶ms.Confirm)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter confirm: %s", err))
|
||||
}
|
||||
|
||||
// ------------- Optional query parameter "verbose" -------------
|
||||
|
||||
err = runtime.BindQueryParameter("form", true, false, "verbose", ctx.QueryParams(), ¶ms.Verbose)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter verbose: %s", err))
|
||||
}
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.DeleteClient(ctx, id, params)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetClient converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GetClient(ctx echo.Context) error {
|
||||
var err error
|
||||
@@ -2104,32 +2180,6 @@ func (w *ServerInterfaceWrapper) UploadDocumentBatch(ctx echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// CancelDocumentBatch converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) CancelDocumentBatch(ctx echo.Context) error {
|
||||
var err error
|
||||
// ------------- Path parameter "id" -------------
|
||||
var id ClientID
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "id", ctx.Param("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err))
|
||||
}
|
||||
|
||||
// ------------- Path parameter "batch_id" -------------
|
||||
var batchId BatchID
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "batch_id", ctx.Param("batch_id"), &batchId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter batch_id: %s", err))
|
||||
}
|
||||
|
||||
ctx.Set(JwtAuthScopes, []string{})
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.CancelDocumentBatch(ctx, id, batchId)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetDocumentBatch converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GetDocumentBatch(ctx echo.Context) error {
|
||||
var err error
|
||||
@@ -2230,6 +2280,33 @@ func (w *ServerInterfaceWrapper) ListClients(ctx echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteDocument converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) DeleteDocument(ctx echo.Context) error {
|
||||
var err error
|
||||
// ------------- Path parameter "id" -------------
|
||||
var id DocumentID
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "id", ctx.Param("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err))
|
||||
}
|
||||
|
||||
ctx.Set(JwtAuthScopes, []string{})
|
||||
|
||||
// Parameter object where we will unmarshal all parameters from the context
|
||||
var params DeleteDocumentParams
|
||||
// ------------- Optional query parameter "verbose" -------------
|
||||
|
||||
err = runtime.BindQueryParameter("form", true, false, "verbose", ctx.QueryParams(), ¶ms.Verbose)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter verbose: %s", err))
|
||||
}
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.DeleteDocument(ctx, id, params)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetDocument converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GetDocument(ctx echo.Context) error {
|
||||
var err error
|
||||
@@ -2431,6 +2508,40 @@ func (w *ServerInterfaceWrapper) CreateFolder(ctx echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteFolder converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) DeleteFolder(ctx echo.Context) error {
|
||||
var err error
|
||||
// ------------- Path parameter "folderId" -------------
|
||||
var folderId openapi_types.UUID
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "folderId", ctx.Param("folderId"), &folderId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter folderId: %s", err))
|
||||
}
|
||||
|
||||
ctx.Set(JwtAuthScopes, []string{})
|
||||
|
||||
// Parameter object where we will unmarshal all parameters from the context
|
||||
var params DeleteFolderParams
|
||||
// ------------- Optional query parameter "include_documents" -------------
|
||||
|
||||
err = runtime.BindQueryParameter("form", true, false, "include_documents", ctx.QueryParams(), ¶ms.IncludeDocuments)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter include_documents: %s", err))
|
||||
}
|
||||
|
||||
// ------------- Optional query parameter "verbose" -------------
|
||||
|
||||
err = runtime.BindQueryParameter("form", true, false, "verbose", ctx.QueryParams(), ¶ms.Verbose)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter verbose: %s", err))
|
||||
}
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.DeleteFolder(ctx, folderId, params)
|
||||
return err
|
||||
}
|
||||
|
||||
// RenameFolder converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) RenameFolder(ctx echo.Context) error {
|
||||
var err error
|
||||
@@ -2634,6 +2745,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
||||
router.POST(baseURL+"/admin/users/:email/disable", wrapper.DisableAdminUser)
|
||||
router.POST(baseURL+"/admin/users/:email/enable", wrapper.EnableAdminUser)
|
||||
router.POST(baseURL+"/client", wrapper.CreateClient)
|
||||
router.DELETE(baseURL+"/client/:id", wrapper.DeleteClient)
|
||||
router.GET(baseURL+"/client/:id", wrapper.GetClient)
|
||||
router.PATCH(baseURL+"/client/:id", wrapper.UpdateClient)
|
||||
router.GET(baseURL+"/client/:id/collector", wrapper.GetCollectorByClientId)
|
||||
@@ -2642,12 +2754,12 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
||||
router.POST(baseURL+"/client/:id/document", wrapper.UploadDocument)
|
||||
router.GET(baseURL+"/client/:id/document/batch", wrapper.ListDocumentBatches)
|
||||
router.POST(baseURL+"/client/:id/document/batch", wrapper.UploadDocumentBatch)
|
||||
router.DELETE(baseURL+"/client/:id/document/batch/:batch_id", wrapper.CancelDocumentBatch)
|
||||
router.GET(baseURL+"/client/:id/document/batch/:batch_id", wrapper.GetDocumentBatch)
|
||||
router.POST(baseURL+"/client/:id/export", wrapper.TriggerExport)
|
||||
router.GET(baseURL+"/client/:id/folders", wrapper.ListClientFolders)
|
||||
router.GET(baseURL+"/client/:id/status", wrapper.GetStatusByClientId)
|
||||
router.GET(baseURL+"/clients", wrapper.ListClients)
|
||||
router.DELETE(baseURL+"/document/:id", wrapper.DeleteDocument)
|
||||
router.GET(baseURL+"/document/:id", wrapper.GetDocument)
|
||||
router.GET(baseURL+"/documents/:documentId/labels", wrapper.GetDocumentLabels)
|
||||
router.POST(baseURL+"/documents/:documentId/labels", wrapper.ApplyLabel)
|
||||
@@ -2660,6 +2772,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
||||
router.GET(baseURL+"/field-extractions/history", wrapper.GetFieldExtractionHistory)
|
||||
router.GET(baseURL+"/field-extractions/version", wrapper.GetFieldExtractionByVersion)
|
||||
router.POST(baseURL+"/folders", wrapper.CreateFolder)
|
||||
router.DELETE(baseURL+"/folders/:folderId", wrapper.DeleteFolder)
|
||||
router.PATCH(baseURL+"/folders/:folderId", wrapper.RenameFolder)
|
||||
router.GET(baseURL+"/folders/:folderId/documents", wrapper.GetFolderDocuments)
|
||||
router.GET(baseURL+"/folders/:folderId/metrics", wrapper.GetFolderMetrics)
|
||||
@@ -2675,276 +2788,284 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||
var swaggerSpec = []string{
|
||||
|
||||
"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",
|
||||
"sQEA",
|
||||
"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",
|
||||
"RzcHwbdP3/7/AAAA///pcQ5AzMEBAA==",
|
||||
}
|
||||
|
||||
// GetSwagger returns the content of the embedded swagger specification file
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
// delete.go implements the hard-delete API handlers for client, document, and folder resources.
|
||||
// These handlers permanently remove data from the database. Source documents in S3 are
|
||||
// intentionally NOT deleted; their paths are logged and optionally returned in verbose mode.
|
||||
// Delete operations are restricted to super_admin via Permit.io middleware.
|
||||
package queryapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"queryorchestration/internal/harddelete"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
openapi_types "github.com/oapi-codegen/runtime/types"
|
||||
)
|
||||
|
||||
// DeleteClient permanently deletes a client and all associated data.
|
||||
// DELETE /client/{id}?confirm=true&verbose=false
|
||||
// Requires confirm=true query parameter as a safety guard.
|
||||
// When verbose=true, returns 200 with S3 paths of orphaned source documents.
|
||||
// When verbose=false (default), returns 204 No Content.
|
||||
func (s *Controllers) DeleteClient(ctx echo.Context, id ClientID, params DeleteClientParams) error {
|
||||
if !params.Confirm {
|
||||
return ctx.JSON(http.StatusBadRequest, ErrorMessage{
|
||||
Message: "confirm=true query parameter is required to delete a client",
|
||||
})
|
||||
}
|
||||
|
||||
adminUser, err := getAdminUserForAudit(ctx)
|
||||
if err != nil {
|
||||
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
|
||||
Message: "Could not identify admin user",
|
||||
})
|
||||
}
|
||||
|
||||
slog.Info("client hard-delete requested",
|
||||
"clientId", id,
|
||||
"requestedBy", adminUser.Email,
|
||||
)
|
||||
|
||||
result, err := s.svc.Client.HardDelete(ctx.Request().Context(), string(id))
|
||||
if err != nil {
|
||||
return handleDeleteError(ctx, err, "client", string(id))
|
||||
}
|
||||
|
||||
slog.Info("client hard-deleted",
|
||||
"clientId", id,
|
||||
"requestedBy", adminUser.Email,
|
||||
"orphanedS3Objects", len(result),
|
||||
)
|
||||
|
||||
return respondDelete(ctx, params.Verbose, result)
|
||||
}
|
||||
|
||||
// DeleteDocument permanently deletes a document and all dependent data.
|
||||
// DELETE /document/{id}?verbose=false
|
||||
// When verbose=true, returns 200 with S3 paths of orphaned source documents.
|
||||
// When verbose=false (default), returns 204 No Content.
|
||||
func (s *Controllers) DeleteDocument(ctx echo.Context, id DocumentID, params DeleteDocumentParams) error {
|
||||
adminUser, err := getAdminUserForAudit(ctx)
|
||||
if err != nil {
|
||||
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
|
||||
Message: "Could not identify admin user",
|
||||
})
|
||||
}
|
||||
|
||||
docUUID := uuid.UUID(id)
|
||||
|
||||
slog.Info("document hard-delete requested",
|
||||
"documentId", docUUID,
|
||||
"requestedBy", adminUser.Email,
|
||||
)
|
||||
|
||||
result, err := s.svc.Document.HardDelete(ctx.Request().Context(), docUUID)
|
||||
if err != nil {
|
||||
return handleDeleteError(ctx, err, "document", docUUID.String())
|
||||
}
|
||||
|
||||
slog.Info("document hard-deleted",
|
||||
"documentId", docUUID,
|
||||
"requestedBy", adminUser.Email,
|
||||
"orphanedS3Objects", len(result),
|
||||
)
|
||||
|
||||
return respondDelete(ctx, params.Verbose, result)
|
||||
}
|
||||
|
||||
// DeleteFolder permanently deletes a folder tree and optionally its documents.
|
||||
// DELETE /folders/{folderId}?include_documents=false&verbose=false
|
||||
// The root folder (path '/') cannot be deleted (returns 409).
|
||||
// If the folder tree contains documents and include_documents is false, returns 409.
|
||||
// When verbose=true, returns 200 with S3 paths of orphaned source documents.
|
||||
// When verbose=false (default), returns 204 No Content.
|
||||
func (s *Controllers) DeleteFolder(ctx echo.Context, folderId openapi_types.UUID, params DeleteFolderParams) error {
|
||||
adminUser, err := getAdminUserForAudit(ctx)
|
||||
if err != nil {
|
||||
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
|
||||
Message: "Could not identify admin user",
|
||||
})
|
||||
}
|
||||
|
||||
includeDocuments := params.IncludeDocuments != nil && *params.IncludeDocuments
|
||||
|
||||
slog.Info("folder hard-delete requested",
|
||||
"folderId", folderId,
|
||||
"includeDocuments", includeDocuments,
|
||||
"requestedBy", adminUser.Email,
|
||||
)
|
||||
|
||||
result, err := s.svc.Folder.HardDelete(ctx.Request().Context(), uuid.UUID(folderId), includeDocuments)
|
||||
if err != nil {
|
||||
return handleDeleteError(ctx, err, "folder", uuid.UUID(folderId).String())
|
||||
}
|
||||
|
||||
slog.Info("folder hard-deleted",
|
||||
"folderId", folderId,
|
||||
"includeDocuments", includeDocuments,
|
||||
"requestedBy", adminUser.Email,
|
||||
"orphanedS3Objects", len(result),
|
||||
)
|
||||
|
||||
return respondDelete(ctx, params.Verbose, result)
|
||||
}
|
||||
|
||||
// handleDeleteError maps service-layer errors to appropriate HTTP responses.
|
||||
// Returns 404 for not-found, 409 for conflict, and 500 for unexpected errors.
|
||||
func handleDeleteError(ctx echo.Context, err error, resourceType string, resourceID string) error {
|
||||
var conflictErr *harddelete.ConflictError
|
||||
if errors.As(err, &conflictErr) {
|
||||
return ctx.JSON(http.StatusConflict, ErrorMessage{
|
||||
Message: conflictErr.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
var notFoundErr *harddelete.NotFoundError
|
||||
if errors.As(err, ¬FoundErr) {
|
||||
return ctx.JSON(http.StatusNotFound, ErrorMessage{
|
||||
Message: notFoundErr.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
slog.Error("hard-delete failed",
|
||||
"resourceType", resourceType,
|
||||
"resourceId", resourceID,
|
||||
"error", err,
|
||||
)
|
||||
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
||||
Message: fmt.Sprintf("failed to delete %s: internal error", resourceType),
|
||||
})
|
||||
}
|
||||
|
||||
// respondDelete sends the appropriate HTTP response based on the verbose flag.
|
||||
// When verbose is true, returns 200 with a DeleteResponse containing S3 paths.
|
||||
// When verbose is false or nil, returns 204 No Content.
|
||||
func respondDelete(ctx echo.Context, verbose *bool, s3Paths []harddelete.S3PathInfo) error {
|
||||
if verbose != nil && *verbose {
|
||||
docs := make([]struct {
|
||||
DocumentId openapi_types.UUID `json:"documentId"`
|
||||
S3Path string `json:"s3Path"`
|
||||
}, len(s3Paths))
|
||||
|
||||
for i, p := range s3Paths {
|
||||
docs[i].DocumentId = openapi_types.UUID(p.DocumentID)
|
||||
docs[i].S3Path = fmt.Sprintf("s3://%s/%s", p.Bucket, p.Key)
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, DeleteResponse{
|
||||
DeletedDocuments: docs,
|
||||
})
|
||||
}
|
||||
|
||||
return ctx.NoContent(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
package queryapi_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
queryapi "queryorchestration/api/queryAPI"
|
||||
"queryorchestration/internal/cognitoauth"
|
||||
"queryorchestration/internal/database/repository"
|
||||
"queryorchestration/internal/test"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
openapi_types "github.com/oapi-codegen/runtime/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// createDeleteContext creates an Echo context with DELETE method and admin user info set.
|
||||
// This simulates an authenticated admin making a delete request.
|
||||
func createDeleteContext(t testing.TB) (echo.Context, *httptest.ResponseRecorder) {
|
||||
t.Helper()
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(http.MethodDelete, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
ctx := e.NewContext(req, rec)
|
||||
ctx.Set("user_info", cognitoauth.UserInfo{
|
||||
Email: "admin@test.local",
|
||||
Username: "admin",
|
||||
Groups: []string{"admin"},
|
||||
})
|
||||
return ctx, rec
|
||||
}
|
||||
|
||||
// seedClientWithDocument creates a client, a root folder (via CreateTestClient), a sub-folder,
|
||||
// and a document with an S3 entry. Returns the document ID and sub-folder ID.
|
||||
func seedClientWithDocument(t *testing.T, cfg *ControllerConfig, clientID string) (uuid.UUID, uuid.UUID) {
|
||||
t.Helper()
|
||||
ctx := t.Context()
|
||||
q := cfg.GetDBQueries()
|
||||
|
||||
test.CreateTestClient(t, cfg, clientID, "Test Client "+clientID)
|
||||
|
||||
// Get root folder created by CreateTestClient
|
||||
rootFolder, err := q.GetFolderByPath(ctx, &repository.GetFolderByPathParams{
|
||||
Clientid: clientID,
|
||||
Path: "/",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a sub-folder
|
||||
subFolder, err := q.CreateFolder(ctx, &repository.CreateFolderParams{
|
||||
Path: "/test-folder",
|
||||
Clientid: clientID,
|
||||
Createdby: "admin@test.local",
|
||||
Parentid: &rootFolder.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a document in the sub-folder
|
||||
filename := "test-doc.pdf"
|
||||
docID, err := q.CreateDocument(ctx, &repository.CreateDocumentParams{
|
||||
Clientid: clientID,
|
||||
Hash: "hash-" + clientID,
|
||||
Filename: &filename,
|
||||
Folderid: &subFolder.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add an S3 entry for the document
|
||||
err = q.AddDocumentEntry(ctx, &repository.AddDocumentEntryParams{
|
||||
Documentid: docID,
|
||||
Bucket: "test-bucket",
|
||||
Key: "test-key/" + clientID + "/doc.pdf",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return docID, subFolder.ID
|
||||
}
|
||||
|
||||
func TestDeleteDocument_Success(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
clientID := "del-doc-success"
|
||||
docID, _ := seedClientWithDocument(t, cfg, clientID)
|
||||
|
||||
ctx, rec := createDeleteContext(t)
|
||||
|
||||
err := cons.DeleteDocument(ctx, openapi_types.UUID(docID), queryapi.DeleteDocumentParams{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNoContent, rec.Code)
|
||||
assert.Empty(t, rec.Body.String())
|
||||
|
||||
// Verify document is actually gone
|
||||
_, dbErr := cfg.GetDBQueries().GetDocumentSummary(t.Context(), docID)
|
||||
require.Error(t, dbErr)
|
||||
}
|
||||
|
||||
// ptrBool returns a pointer to a bool value.
|
||||
func ptrBool(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
func TestDeleteDocument_Verbose(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
clientID := "del-doc-verbose"
|
||||
docID, _ := seedClientWithDocument(t, cfg, clientID)
|
||||
|
||||
ctx, rec := createDeleteContext(t)
|
||||
|
||||
err := cons.DeleteDocument(ctx, openapi_types.UUID(docID), queryapi.DeleteDocumentParams{
|
||||
Verbose: ptrBool(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var resp queryapi.DeleteResponse
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, resp.DeletedDocuments, 1)
|
||||
assert.Equal(t, openapi_types.UUID(docID), resp.DeletedDocuments[0].DocumentId)
|
||||
assert.Contains(t, resp.DeletedDocuments[0].S3Path, "s3://test-bucket/test-key/")
|
||||
}
|
||||
|
||||
func TestDeleteDocument_NotFound(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
ctx, rec := createDeleteContext(t)
|
||||
|
||||
fakeID := uuid.New()
|
||||
err := cons.DeleteDocument(ctx, openapi_types.UUID(fakeID), queryapi.DeleteDocumentParams{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, rec.Code)
|
||||
|
||||
var resp queryapi.ErrorMessage
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, resp.Message, "not found")
|
||||
}
|
||||
|
||||
func TestDeleteClient_MissingConfirm(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
clientID := "del-client-noconfirm"
|
||||
test.CreateTestClient(t, cfg, clientID, "Test Client")
|
||||
|
||||
ctx, rec := createDeleteContext(t)
|
||||
|
||||
err := cons.DeleteClient(ctx, queryapi.ClientID(clientID), queryapi.DeleteClientParams{
|
||||
Confirm: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
|
||||
var resp queryapi.ErrorMessage
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, resp.Message, "confirm=true")
|
||||
}
|
||||
|
||||
func TestDeleteClient_Success(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
clientID := "del-client-ok"
|
||||
seedClientWithDocument(t, cfg, clientID)
|
||||
|
||||
ctx, rec := createDeleteContext(t)
|
||||
|
||||
err := cons.DeleteClient(ctx, queryapi.ClientID(clientID), queryapi.DeleteClientParams{
|
||||
Confirm: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNoContent, rec.Code)
|
||||
assert.Empty(t, rec.Body.String())
|
||||
|
||||
// Verify client is gone
|
||||
_, dbErr := cfg.GetDBQueries().GetClient(t.Context(), clientID)
|
||||
require.Error(t, dbErr)
|
||||
}
|
||||
|
||||
func TestDeleteClient_Verbose(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
clientID := "del-client-verbose"
|
||||
docID, _ := seedClientWithDocument(t, cfg, clientID)
|
||||
|
||||
ctx, rec := createDeleteContext(t)
|
||||
|
||||
err := cons.DeleteClient(ctx, queryapi.ClientID(clientID), queryapi.DeleteClientParams{
|
||||
Confirm: true,
|
||||
Verbose: ptrBool(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var resp queryapi.DeleteResponse
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, resp.DeletedDocuments, 1)
|
||||
assert.Equal(t, openapi_types.UUID(docID), resp.DeletedDocuments[0].DocumentId)
|
||||
assert.Contains(t, resp.DeletedDocuments[0].S3Path, "s3://test-bucket/")
|
||||
}
|
||||
|
||||
func TestDeleteFolder_Success(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
clientID := "del-folder-ok"
|
||||
test.CreateTestClient(t, cfg, clientID, "Test Client")
|
||||
|
||||
q := cfg.GetDBQueries()
|
||||
rootFolder, err := q.GetFolderByPath(t.Context(), &repository.GetFolderByPathParams{
|
||||
Clientid: clientID,
|
||||
Path: "/",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
subFolder, err := q.CreateFolder(t.Context(), &repository.CreateFolderParams{
|
||||
Path: "/empty-folder",
|
||||
Clientid: clientID,
|
||||
Createdby: "admin@test.local",
|
||||
Parentid: &rootFolder.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, rec := createDeleteContext(t)
|
||||
|
||||
err = cons.DeleteFolder(ctx, openapi_types.UUID(subFolder.ID), queryapi.DeleteFolderParams{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNoContent, rec.Code)
|
||||
assert.Empty(t, rec.Body.String())
|
||||
|
||||
// Verify folder is gone
|
||||
_, dbErr := q.GetFolderByID(t.Context(), subFolder.ID)
|
||||
require.Error(t, dbErr)
|
||||
}
|
||||
|
||||
func TestDeleteFolder_RootFolderConflict(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
clientID := "del-folder-root"
|
||||
test.CreateTestClient(t, cfg, clientID, "Test Client")
|
||||
|
||||
q := cfg.GetDBQueries()
|
||||
rootFolder, err := q.GetFolderByPath(t.Context(), &repository.GetFolderByPathParams{
|
||||
Clientid: clientID,
|
||||
Path: "/",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, rec := createDeleteContext(t)
|
||||
|
||||
err = cons.DeleteFolder(ctx, openapi_types.UUID(rootFolder.ID), queryapi.DeleteFolderParams{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusConflict, rec.Code)
|
||||
|
||||
var resp queryapi.ErrorMessage
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, resp.Message, "root folder")
|
||||
}
|
||||
|
||||
func TestDeleteFolder_DocumentsWithoutInclude(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
clientID := "del-folder-nodocs"
|
||||
_, folderID := seedClientWithDocument(t, cfg, clientID)
|
||||
|
||||
ctx, rec := createDeleteContext(t)
|
||||
|
||||
err := cons.DeleteFolder(ctx, openapi_types.UUID(folderID), queryapi.DeleteFolderParams{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusConflict, rec.Code)
|
||||
|
||||
var resp queryapi.ErrorMessage
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, resp.Message, "documents")
|
||||
}
|
||||
|
||||
func TestDeleteFolder_IncludeDocumentsCascade(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
clientID := "del-folder-cascade"
|
||||
docID, folderID := seedClientWithDocument(t, cfg, clientID)
|
||||
|
||||
ctx, rec := createDeleteContext(t)
|
||||
|
||||
err := cons.DeleteFolder(ctx, openapi_types.UUID(folderID), queryapi.DeleteFolderParams{
|
||||
IncludeDocuments: ptrBool(true),
|
||||
Verbose: ptrBool(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var resp queryapi.DeleteResponse
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, resp.DeletedDocuments, 1)
|
||||
assert.Equal(t, openapi_types.UUID(docID), resp.DeletedDocuments[0].DocumentId)
|
||||
|
||||
// Verify both folder and document are gone
|
||||
q := cfg.GetDBQueries()
|
||||
_, dbErr := q.GetFolderByID(t.Context(), folderID)
|
||||
require.Error(t, dbErr)
|
||||
_, dbErr = q.GetDocumentSummary(t.Context(), docID)
|
||||
require.Error(t, dbErr)
|
||||
}
|
||||
@@ -288,19 +288,3 @@ func (s *Controllers) GetDocumentBatch(ctx echo.Context, clientId ClientID, batc
|
||||
|
||||
return ctx.JSON(http.StatusOK, details)
|
||||
}
|
||||
|
||||
// CancelDocumentBatch cancels a batch upload in progress
|
||||
func (s *Controllers) CancelDocumentBatch(ctx echo.Context, clientId ClientID, batchId BatchID) error {
|
||||
// BatchID is already a UUID type
|
||||
batchUUID := uuid.UUID(batchId)
|
||||
|
||||
// Cancel the batch
|
||||
err := s.svc.DocumentBatch.Cancel(ctx.Request().Context(), clientId, batchUUID)
|
||||
if err != nil {
|
||||
slog.Error("unable to cancel batch", "error", err, "batchId", batchId)
|
||||
return echo.NewHTTPError(http.StatusNotFound, "batch not found or cannot be canceled")
|
||||
}
|
||||
|
||||
// Return 204 No Content for successful cancellation
|
||||
return ctx.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -418,41 +418,6 @@ func TestGetDocumentBatch(t *testing.T) {
|
||||
assert.Equal(t, "test.zip", response.OriginalFilename)
|
||||
}
|
||||
|
||||
func TestCancelDocumentBatch(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
// Create test client with root folder
|
||||
clientID := "test_client_cancel"
|
||||
test.CreateTestClient(t, cfg, clientID, "Test Client Batch Cancel")
|
||||
|
||||
// Create test batch
|
||||
batchID, err := cfg.GetDBQueries().CreateBatchUpload(t.Context(), &repository.CreateBatchUploadParams{
|
||||
ClientID: clientID,
|
||||
OriginalFilename: "test.zip",
|
||||
TotalDocuments: 10,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test canceling batch
|
||||
ctx, rec := createContext(t)
|
||||
err = cons.CancelDocumentBatch(ctx, clientID, queryapi.BatchID(batchID))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNoContent, rec.Code)
|
||||
|
||||
// Verify batch was canceled
|
||||
batch, err := cfg.GetDBQueries().GetBatchUpload(t.Context(), &repository.GetBatchUploadParams{
|
||||
ID: batchID,
|
||||
ClientID: clientID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, repository.BatchStatusCancelled, batch.Status)
|
||||
}
|
||||
|
||||
func TestUploadDocumentBatch_Part1_Success(t *testing.T) {
|
||||
// Setup: Create testcontainers (database + localstack)
|
||||
cfg := &ControllerConfig{}
|
||||
|
||||
@@ -61,6 +61,7 @@ resources:
|
||||
- get
|
||||
- post
|
||||
- patch
|
||||
- delete
|
||||
|
||||
- name: labels
|
||||
description: Label-based document lookup
|
||||
@@ -105,6 +106,7 @@ roles:
|
||||
- get
|
||||
- post
|
||||
- patch
|
||||
- delete
|
||||
labels:
|
||||
- get
|
||||
|
||||
@@ -157,7 +159,6 @@ roles:
|
||||
- get
|
||||
- post
|
||||
- patch
|
||||
- delete
|
||||
clients:
|
||||
- get
|
||||
document:
|
||||
@@ -216,6 +217,7 @@ policy_mappings:
|
||||
methods:
|
||||
GET: client:get
|
||||
PATCH: client:patch
|
||||
DELETE: client:delete
|
||||
- path: /client/{id}/status
|
||||
methods:
|
||||
GET: client:get
|
||||
@@ -251,6 +253,7 @@ policy_mappings:
|
||||
- path: /document/{id}
|
||||
methods:
|
||||
GET: document:get
|
||||
DELETE: document:delete
|
||||
|
||||
export_endpoints:
|
||||
- path: /export/{id}
|
||||
@@ -285,6 +288,7 @@ policy_mappings:
|
||||
- path: /folders/{folderId}
|
||||
methods:
|
||||
PATCH: folders:patch
|
||||
DELETE: folders:delete
|
||||
- path: /folders/{folderId}/documents
|
||||
methods:
|
||||
GET: folders:get
|
||||
|
||||
@@ -266,6 +266,36 @@ Updates client information.
|
||||
|
||||
**Response**: `200` Client updated successfully
|
||||
|
||||
### `DELETE /client/{id}`
|
||||
|
||||
Permanently deletes a client and all associated data: documents (with all dependent rows), folders, collector configurations, batch uploads, and clientCanSync records. Source documents in S3 are NOT deleted; their paths are logged and optionally returned. This operation is irreversible.
|
||||
|
||||
**Security**: Requires JWT authentication. Restricted to `super_admin` role only.
|
||||
|
||||
**Query Parameters**:
|
||||
|
||||
- `confirm` (required, boolean): Must be `true` to proceed. Returns `400 Bad Request` if absent or false.
|
||||
- `verbose` (optional, boolean, default: false): When `true`, returns S3 paths of orphaned source documents.
|
||||
|
||||
**Response**:
|
||||
|
||||
- `204`: Client deleted successfully (when `verbose=false`)
|
||||
- `200`: Client deleted successfully with S3 paths (when `verbose=true`):
|
||||
|
||||
```json
|
||||
{
|
||||
"deletedDocuments": [
|
||||
{
|
||||
"documentId": "019580df-b3f8-7348-9ab1-1e55b3f18ed7",
|
||||
"s3Path": "s3://bucket-name/key/path/to/file"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `400`: Missing `confirm=true` query parameter
|
||||
- `404`: Client not found
|
||||
|
||||
### `GET /client/{id}/status`
|
||||
|
||||
Returns client synchronization status.
|
||||
@@ -356,6 +386,38 @@ Retrieves enriched document details by ID, including metadata such as folder, fi
|
||||
}
|
||||
```
|
||||
|
||||
### `DELETE /document/{id}`
|
||||
|
||||
Permanently deletes a document and all dependent data: document entries, cleans (and their entries), text extractions (and their entries), field extractions (versions and array fields), labels, results, and result dependencies. Source documents in S3 are NOT deleted; their paths are logged and optionally returned.
|
||||
|
||||
**Security**: Requires JWT authentication. Restricted to `super_admin` role only.
|
||||
|
||||
**Path Parameters**:
|
||||
|
||||
- `id`: Document UUID (max 36 chars)
|
||||
|
||||
**Query Parameters**:
|
||||
|
||||
- `verbose` (optional, boolean, default: false): When `true`, returns S3 paths of orphaned source documents.
|
||||
|
||||
**Response**:
|
||||
|
||||
- `204`: Document deleted successfully (when `verbose=false`)
|
||||
- `200`: Document deleted successfully with S3 paths (when `verbose=true`):
|
||||
|
||||
```json
|
||||
{
|
||||
"deletedDocuments": [
|
||||
{
|
||||
"documentId": "019580df-b3f8-7348-9ab1-1e55b3f18ed7",
|
||||
"s3Path": "s3://bucket-name/key/path/to/file"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `404`: Document not found
|
||||
|
||||
---
|
||||
|
||||
## Batch Document Operations (DocumentsService)
|
||||
@@ -448,17 +510,6 @@ Retrieves detailed status information for a specific batch upload.
|
||||
}
|
||||
```
|
||||
|
||||
### `DELETE /client/{id}/document/batch/{batch_id}`
|
||||
|
||||
Cancels a batch upload that is currently processing.
|
||||
|
||||
**Security**: Requires JWT authentication
|
||||
|
||||
**Response**:
|
||||
|
||||
- `204`: Batch upload cancelled successfully
|
||||
- `404`: Batch not found
|
||||
|
||||
---
|
||||
|
||||
## Folder Operations (FolderService)
|
||||
@@ -612,6 +663,40 @@ Renames an existing folder.
|
||||
|
||||
**Response** (`200 OK`): Updated folder object
|
||||
|
||||
### `DELETE /folders/{folderId}`
|
||||
|
||||
Permanently deletes a folder and all child folders recursively. The root folder (`/`) cannot be deleted. If the folder tree contains documents, the caller must either delete the documents first or pass `include_documents=true`.
|
||||
|
||||
**Security**: Requires JWT authentication. Restricted to `super_admin` role only.
|
||||
|
||||
**Path Parameters**:
|
||||
|
||||
- `folderId`: Folder UUID
|
||||
|
||||
**Query Parameters**:
|
||||
|
||||
- `include_documents` (optional, boolean, default: false): When `true`, deletes all documents in the folder tree along with the folders. When `false` and the folder tree contains documents, returns `409 Conflict`.
|
||||
- `verbose` (optional, boolean, default: false): When `true`, returns S3 paths of orphaned source documents.
|
||||
|
||||
**Response**:
|
||||
|
||||
- `204`: Folder tree deleted successfully (when `verbose=false`)
|
||||
- `200`: Folder tree deleted successfully with S3 paths (when `verbose=true`):
|
||||
|
||||
```json
|
||||
{
|
||||
"deletedDocuments": [
|
||||
{
|
||||
"documentId": "019580df-b3f8-7348-9ab1-1e55b3f18ed7",
|
||||
"s3Path": "s3://bucket-name/key/path/to/file"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `404`: Folder not found
|
||||
- `409`: Cannot delete root folder, or folder tree contains documents and `include_documents` is not `true`
|
||||
|
||||
### `GET /folders/{folderId}/documents`
|
||||
|
||||
Retrieves all documents within a specific folder, including filename and labels. Returns a lighter-weight format than `GET /document/{id}` (excludes `client_id`).
|
||||
@@ -1637,20 +1722,22 @@ The following table shows what data is passed to Permit.io for each protected en
|
||||
| `GET` | `/clients` | `clients` | `get` | List all clients |
|
||||
| `GET` | `/client/{id}` | `client` | `get` | Get client details |
|
||||
| `PATCH` | `/client/{id}` | `client` | `patch` | Update client |
|
||||
| `DELETE` | `/client/{id}` | `client` | `delete` | Hard-delete client (super_admin only) |
|
||||
| `GET` | `/client/{id}/status` | `client` | `get` | Get client sync status |
|
||||
| **Document Operations** |
|
||||
| `POST` | `/client/{id}/document` | `client` | `post` | Upload document |
|
||||
| `GET` | `/client/{id}/document` | `client` | `get` | List client documents |
|
||||
| `GET` | `/document/{id}` | `document` | `get` | Get document details |
|
||||
| `DELETE` | `/document/{id}` | `document` | `delete` | Hard-delete document (super_admin only) |
|
||||
| **Batch Document Operations** |
|
||||
| `POST` | `/client/{id}/document/batch` | `client` | `post` | Upload batch (ZIP) |
|
||||
| `GET` | `/client/{id}/document/batch` | `client` | `get` | List batch uploads |
|
||||
| `GET` | `/client/{id}/document/batch/{batch_id}` | `client` | `get` | Get batch status |
|
||||
| `DELETE` | `/client/{id}/document/batch/{batch_id}` | `client` | `delete` | Cancel batch upload |
|
||||
| **Folder Operations** |
|
||||
| `GET` | `/client/{id}/folders` | `client` | `get` | List client folders |
|
||||
| `POST` | `/folders` | `folders` | `post` | Create folder |
|
||||
| `PATCH` | `/folders/{folderId}` | `folders` | `patch` | Rename folder |
|
||||
| `DELETE` | `/folders/{folderId}` | `folders` | `delete` | Hard-delete folder tree (super_admin only) |
|
||||
| `GET` | `/folders/{folderId}/documents` | `folders` | `get` | List folder documents |
|
||||
| `GET` | `/folders/{folderId}/metrics` | `folders` | `get` | Get folder metrics |
|
||||
| **Label Operations** |
|
||||
@@ -1717,16 +1804,18 @@ To configure Permit.io to work with this API, administrators must create:
|
||||
| `delete` | DELETE | Delete resources |
|
||||
| `head` | HEAD | Check resource existence |
|
||||
|
||||
#### Example Role Configuration
|
||||
#### Role Configuration
|
||||
|
||||
Typical role assignments for common personas:
|
||||
The following roles are defined in the Permit.io configuration (`cmd/auth_related/permit.setup/permit_policies.yaml`):
|
||||
|
||||
| Role | Resources | Actions | Use Case |
|
||||
| ------------ | -------------------------------------------------------------------------- | ---------------------- | -------------------------- |
|
||||
| `viewer` | `client`, `clients`, `document`, `documents`, `folders`, `export` | `get` | Read-only access |
|
||||
| `editor` | `client`, `document`, `documents`, `folders`, `field-extractions` | `get`, `post`, `patch` | Content management |
|
||||
| `admin` | All resources | All actions | Full administrative access |
|
||||
| `user_admin` | `admin` | All actions | User management only |
|
||||
| Role | Resources | Actions | Use Case |
|
||||
| -------------- | -------------------------------------------------------------------------------------- | -------------------------------- | --------------------------------------- |
|
||||
| `super_admin` | All resources | All actions including `delete` | Full administrative access |
|
||||
| `user_admin` | `admin`, `documents`, `field-extractions`, `folders` | `get`, `post`, `patch`, `delete` (admin only) | User management |
|
||||
| `auditor` | `admin`, `client`, `clients`, `document`, `export`, `documents`, `field-extractions`, `folders`, `labels` | `get` (and `head` for admin) | Read-only access for auditing |
|
||||
| `client_user` | `client`, `clients`, `document`, `export`, `documents`, `field-extractions`, `folders`, `labels` | `get`, `post`, `patch` (varies by resource) | Client-specific access |
|
||||
|
||||
**Delete permissions**: Only `super_admin` has `delete` access to `client`, `document`, and `folders` resources. The `client_user` role does NOT have `client:delete` or any other delete permissions on these resources.
|
||||
|
||||
### Authorization Error Responses
|
||||
|
||||
|
||||
@@ -309,10 +309,10 @@ Permissions follow the format `resource:action`:
|
||||
|
||||
| Resource | Actions | Description |
|
||||
|----------|---------|-------------|
|
||||
| `client` | `get`, `post`, `patch`, `delete` | Client management |
|
||||
| `client` | `get`, `post`, `patch`, `delete` | Client management (delete: super_admin only) |
|
||||
| `clients` | `get` | List all clients |
|
||||
| `document` | `get`, `post`, `patch`, `delete` | Document operations |
|
||||
| `folders` | `get`, `post`, `patch`, `delete` | Folder management |
|
||||
| `document` | `get`, `post`, `delete` | Document operations (delete: super_admin only) |
|
||||
| `folders` | `get`, `post`, `patch`, `delete` | Folder management (delete: super_admin only) |
|
||||
| `export` | `get`, `post` | Export operations |
|
||||
| `admin` | `get`, `post`, `patch`, `delete` | User and EULA administration |
|
||||
| `eula` | `get`, `post` | EULA status and agreement |
|
||||
@@ -402,6 +402,9 @@ const UI_FEATURES = {
|
||||
viewDocuments: ['client:get', 'document:get'],
|
||||
uploadDocuments: ['client:post'],
|
||||
manageClients: ['client:post', 'client:patch'],
|
||||
deleteClients: ['client:delete'],
|
||||
deleteDocuments: ['document:delete'],
|
||||
deleteFolders: ['folders:delete'],
|
||||
viewExports: ['export:get'],
|
||||
createExports: ['export:post'],
|
||||
adminPanel: ['admin:get'],
|
||||
@@ -431,6 +434,7 @@ function canAccessFeature(
|
||||
| `401` | Unauthorized | Token invalid/expired - refresh or re-login |
|
||||
| `403` | Forbidden | User lacks permission - show access denied |
|
||||
| `404` | Not Found | Resource doesn't exist |
|
||||
| `409` | Conflict | Operation cannot proceed in current state (e.g., folder has documents) |
|
||||
| `429` | Too Many Requests | Rate limited - implement backoff |
|
||||
| `500` | Server Error | Show generic error, retry later |
|
||||
| `502` | Bad Gateway | Authorization service unavailable |
|
||||
@@ -898,6 +902,9 @@ Content-Type: application/json
|
||||
| `/client/{id}/document` | POST | Upload document (multipart/form-data) |
|
||||
| `/client/{id}/document/batch` | POST | Upload ZIP batch |
|
||||
| `/document/{id}` | GET | Get document details |
|
||||
| `/client/{id}` | DELETE | Hard-delete client (super_admin, requires confirm=true) |
|
||||
| `/document/{id}` | DELETE | Hard-delete document (super_admin) |
|
||||
| `/folders/{folderId}` | DELETE | Hard-delete folder tree (super_admin) |
|
||||
| `/clients/{clientId}/folders` | GET | List folders |
|
||||
| `/eula` | GET | Get current EULA (no auth required) |
|
||||
| `/eula/status` | GET | Check user's EULA agreement status |
|
||||
|
||||
@@ -13,18 +13,18 @@ This document provides a comprehensive summary of all REST API endpoints availab
|
||||
| /identity | GET | AuthService |
|
||||
| /client | POST | ClientService |
|
||||
| /clients | GET | ClientService |
|
||||
| /client/{id} | GET, PATCH | ClientService |
|
||||
| /client/{id} | GET, PATCH, DELETE | ClientService |
|
||||
| /client/{id}/status | GET | ClientService |
|
||||
| /client/{id}/collector | GET, PATCH | CollectorService |
|
||||
| /client/{id}/folders | GET | FolderService |
|
||||
| /folders | POST | FolderService |
|
||||
| /folders/{folderId} | PATCH | FolderService |
|
||||
| /folders/{folderId} | PATCH, DELETE | FolderService |
|
||||
| /folders/{folderId}/documents | GET | FolderService |
|
||||
| /folders/{folderId}/metrics | GET | FolderService |
|
||||
| /client/{id}/document | GET, POST | DocumentsService |
|
||||
| /client/{id}/document/batch | GET, POST | DocumentsService |
|
||||
| /client/{id}/document/batch/{batch_id} | GET, DELETE | DocumentsService |
|
||||
| /document/{id} | GET | DocumentsService |
|
||||
| /client/{id}/document/batch/{batch_id} | GET | DocumentsService |
|
||||
| /document/{id} | GET, DELETE | DocumentsService |
|
||||
| /documents/{documentId}/labels | GET, POST | LabelService |
|
||||
| /labels/{labelName}/documents | GET | LabelService |
|
||||
| /field-extractions | GET, POST | FieldExtractionService |
|
||||
@@ -90,6 +90,14 @@ This document provides a comprehensive summary of all REST API endpoints availab
|
||||
- Request Body: `{name?, can_sync?}` (optional fields)
|
||||
- Returns: 200 OK on success
|
||||
|
||||
- **DELETE /client/{id}** - Hard-delete a client and all associated data (super_admin only)
|
||||
- Query Parameters:
|
||||
- `confirm=true` (required) - Must be set to confirm deletion
|
||||
- `verbose` (optional, default: false) - When true, returns S3 paths of orphaned source documents
|
||||
- Deletes all documents, folders, collector configs, batch uploads, and clientCanSync records
|
||||
- S3 source files are NOT deleted; their paths are logged
|
||||
- Returns: 204 No Content (or 200 with S3 paths when verbose=true)
|
||||
|
||||
- **GET /client/{id}/status** - Retrieve the sync status for a client
|
||||
- Returns: `{status}` where status is one of: `IN_SYNC`, `NOT_SYNCED`, `NOT_SYNCING`
|
||||
|
||||
@@ -120,6 +128,15 @@ This document provides a comprehensive summary of all REST API endpoints availab
|
||||
- Request Body: `{path}`
|
||||
- Returns: `{id, client_id, path, parent_id}` (200 OK)
|
||||
|
||||
- **DELETE /folders/{folderId}** - Hard-delete a folder tree (super_admin only)
|
||||
- Query Parameters:
|
||||
- `include_documents` (optional, default: false) - When true, also deletes all documents in the folder tree
|
||||
- `verbose` (optional, default: false) - When true, returns S3 paths of orphaned source documents
|
||||
- Root folder (`/`) cannot be deleted (returns 409 Conflict)
|
||||
- If folder tree contains documents and include_documents is false, returns 409 Conflict
|
||||
- S3 source files are NOT deleted; their paths are logged
|
||||
- Returns: 204 No Content (or 200 with S3 paths when verbose=true)
|
||||
|
||||
- **GET /folders/{folderId}/documents** - Get documents in a folder
|
||||
- Query Parameters:
|
||||
- `textRecord` (optional, default: false) - When true, includes full text extraction record
|
||||
@@ -146,6 +163,13 @@ This document provides a comprehensive summary of all REST API endpoints availab
|
||||
- `textRecord` (optional, default: false) - When true, includes full text extraction record
|
||||
- Returns: `{id, client_id, hash, folder_id, filename, labels[], fields{}, text_record?}`
|
||||
|
||||
- **DELETE /document/{id}** - Hard-delete a document and all dependent data (super_admin only)
|
||||
- Query Parameters:
|
||||
- `verbose` (optional, default: false) - When true, returns S3 paths of orphaned source documents
|
||||
- Deletes all dependent rows: entries, cleans, text extractions, field extractions, labels, results
|
||||
- S3 source files are NOT deleted; their paths are logged
|
||||
- Returns: 204 No Content (or 200 with S3 paths when verbose=true)
|
||||
|
||||
### Batch Document Processing
|
||||
- **POST /client/{id}/document/batch** - Upload multiple PDFs as ZIP archive for batch processing
|
||||
- Content-Type: `multipart/form-data`
|
||||
@@ -162,9 +186,6 @@ This document provides a comprehensive summary of all REST API endpoints availab
|
||||
- **GET /client/{id}/document/batch/{batch_id}** - Get detailed status of a specific batch upload
|
||||
- Returns: `{batch_id, client_id, original_filename, status, total_documents, processed_documents, failed_documents, invalid_type_documents, progress_percent, created_at, completed_at?, failed_filenames[]}`
|
||||
|
||||
- **DELETE /client/{id}/document/batch/{batch_id}** - Cancel a batch upload currently processing
|
||||
- Returns: 204 No Content on successful cancellation
|
||||
|
||||
## Label Service
|
||||
|
||||
### Document Label Management
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"queryorchestration/internal/database/repository"
|
||||
"queryorchestration/internal/document"
|
||||
"queryorchestration/internal/harddelete"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// HardDelete permanently removes a client and ALL associated data from the database.
|
||||
// This includes all documents (with full cascade), folders, collector configurations,
|
||||
// batch uploads, and sync records. The entire operation executes in a single transaction.
|
||||
//
|
||||
// S3 source files are intentionally NOT deleted. Their paths are collected before
|
||||
// the transaction, logged at INFO level, and returned for optional verbose responses.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: request context
|
||||
// - clientID: the client identifier to delete
|
||||
//
|
||||
// Returns:
|
||||
// - []harddelete.S3PathInfo: S3 paths of orphaned source documents
|
||||
// - error: NotFoundError if client does not exist, or a database error
|
||||
func (s *Service) HardDelete(ctx context.Context, clientID string) ([]harddelete.S3PathInfo, error) {
|
||||
queries := s.cfg.GetDBQueries()
|
||||
|
||||
// Get all document IDs for the client
|
||||
docIDs, err := queries.GetAllDocumentIDsForClient(ctx, clientID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get document IDs for client %s: %w", clientID, err)
|
||||
}
|
||||
|
||||
// Collect S3 paths for all documents before the transaction
|
||||
allS3Paths, err := harddelete.CollectS3PathsForDocuments(ctx, docIDs, wrapS3Query(queries))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.cfg.ExecuteDBTransaction(ctx, func(ctx context.Context, q *repository.Queries) error {
|
||||
// 1. Delete all documents with full cascade
|
||||
for _, docID := range docIDs {
|
||||
if err := document.DeleteDocumentCascade(ctx, q, docID); err != nil {
|
||||
return fmt.Errorf("cascade delete document %s: %w", docID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Delete all documentUploads for the client
|
||||
if err := q.DeleteDocumentUploadsForClient(ctx, clientID); err != nil {
|
||||
return fmt.Errorf("delete document uploads: %w", err)
|
||||
}
|
||||
|
||||
// 3. Delete all folders for the client (safe now that all documents are gone)
|
||||
if err := q.DeleteAllFoldersForClient(ctx, clientID); err != nil {
|
||||
return fmt.Errorf("delete folders: %w", err)
|
||||
}
|
||||
|
||||
// 4-6. Delete collector data, batch uploads, and sync records
|
||||
if err := deleteClientDependencies(ctx, q, clientID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 7. Delete the client row
|
||||
rowsAffected, err := q.HardDeleteClient(ctx, clientID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete client: %w", err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return &harddelete.NotFoundError{
|
||||
ResourceType: "client",
|
||||
ResourceID: clientID,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
harddelete.LogOrphanedS3Paths(allS3Paths)
|
||||
return allS3Paths, nil
|
||||
}
|
||||
|
||||
// clientDependencyDelete pairs a description with a delete function for use in
|
||||
// deleteClientDependencies. Each entry represents one step in the FK-ordered cascade.
|
||||
type clientDependencyDelete struct {
|
||||
name string
|
||||
fn func(ctx context.Context, clientID string) error
|
||||
}
|
||||
|
||||
// deleteClientDependencies removes collector data, batch uploads, and sync records
|
||||
// for a client within an existing transaction. Must be called after documents and
|
||||
// folders are already deleted. Executes deletes in FK dependency order.
|
||||
func deleteClientDependencies(ctx context.Context, q *repository.Queries, clientID string) error {
|
||||
deletes := []clientDependencyDelete{
|
||||
{"collector min clean versions", q.DeleteCollectorMinCleanVersions},
|
||||
{"collector active versions", q.DeleteCollectorActiveVersions},
|
||||
{"collector versions", q.DeleteCollectorVersions},
|
||||
{"batch uploads", q.DeleteBatchUploads},
|
||||
{"client can sync", q.DeleteClientCanSync},
|
||||
}
|
||||
for _, d := range deletes {
|
||||
if err := d.fn(ctx, clientID); err != nil {
|
||||
return fmt.Errorf("delete %s: %w", d.name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// wrapS3Query adapts the repository's CollectDocumentS3Paths to the function signature
|
||||
// expected by harddelete.CollectS3PathsForDocuments.
|
||||
func wrapS3Query(queries *repository.Queries) func(ctx context.Context, docID uuid.UUID) ([]harddelete.S3Row, error) {
|
||||
return func(ctx context.Context, docID uuid.UUID) ([]harddelete.S3Row, error) {
|
||||
rows, err := queries.CollectDocumentS3Paths(ctx, docID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]harddelete.S3Row, len(rows))
|
||||
for i, r := range rows {
|
||||
result[i] = harddelete.S3Row{Bucket: r.Bucket, Key: r.Key}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package client_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"queryorchestration/internal/client"
|
||||
"queryorchestration/internal/database/repository"
|
||||
"queryorchestration/internal/harddelete"
|
||||
"queryorchestration/internal/test"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// createDocWithEntryForClient creates a document with an S3 entry in the given folder for a client.
|
||||
func createDocWithEntryForClient(t *testing.T, cfg *TestConfig, clientID string, folderID *uuid.UUID, hash string) uuid.UUID {
|
||||
t.Helper()
|
||||
ctx := t.Context()
|
||||
q := cfg.GetDBQueries()
|
||||
|
||||
filename := hash + ".pdf"
|
||||
docID, err := q.CreateDocument(ctx, &repository.CreateDocumentParams{
|
||||
Clientid: clientID,
|
||||
Hash: hash,
|
||||
Filename: &filename,
|
||||
Folderid: folderID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = q.AddDocumentEntry(ctx, &repository.AddDocumentEntryParams{
|
||||
Documentid: docID,
|
||||
Bucket: "test-bucket",
|
||||
Key: "clients/" + clientID + "/" + hash,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return docID
|
||||
}
|
||||
|
||||
func TestHardDeleteClient(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
cfg := &TestConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
|
||||
svc := client.New(cfg)
|
||||
q := cfg.GetDBQueries()
|
||||
|
||||
t.Run("deletes client and all associated data", func(t *testing.T) {
|
||||
clientID := "test-client-hard-delete"
|
||||
test.CreateTestClient(t, cfg, clientID, "Test Client Hard Delete")
|
||||
|
||||
// Get root folder
|
||||
rootFolder, err := q.GetFolderByPath(ctx, &repository.GetFolderByPathParams{
|
||||
Clientid: clientID,
|
||||
Path: "/",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create documents with S3 entries
|
||||
doc1ID := createDocWithEntryForClient(t, cfg, clientID, &rootFolder.ID, "client-del-doc1")
|
||||
doc2ID := createDocWithEntryForClient(t, cfg, clientID, &rootFolder.ID, "client-del-doc2")
|
||||
|
||||
// Create collector data
|
||||
versionID, err := q.AddLatestCollectorVersion(ctx, clientID)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = q.SetActiveCollectorVersion(ctx, &repository.SetActiveCollectorVersionParams{
|
||||
Clientid: clientID,
|
||||
Versionid: versionID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = q.SetCollectorCleanVersion(ctx, &repository.SetCollectorCleanVersionParams{
|
||||
Clientid: clientID,
|
||||
Addedversion: versionID,
|
||||
Versionid: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create clientCanSync
|
||||
err = q.AddClientCanSync(ctx, &repository.AddClientCanSyncParams{
|
||||
Cansync: true,
|
||||
Clientid: clientID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Delete the client
|
||||
s3Paths, err := svc.HardDelete(ctx, clientID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify S3 paths returned for both documents
|
||||
assert.Len(t, s3Paths, 2)
|
||||
|
||||
// Verify documents are gone
|
||||
_, err = q.GetDocumentSummary(ctx, doc1ID)
|
||||
require.Error(t, err)
|
||||
_, err = q.GetDocumentSummary(ctx, doc2ID)
|
||||
require.Error(t, err)
|
||||
|
||||
// Verify client is gone
|
||||
_, err = q.GetClient(ctx, clientID)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("returns NotFoundError for non-existent client", func(t *testing.T) {
|
||||
_, err := svc.HardDelete(ctx, "non-existent-client")
|
||||
require.Error(t, err)
|
||||
|
||||
var notFoundErr *harddelete.NotFoundError
|
||||
assert.True(t, errors.As(err, ¬FoundErr))
|
||||
})
|
||||
|
||||
t.Run("deletes client with no documents", func(t *testing.T) {
|
||||
clientID := "test-client-no-docs-del"
|
||||
test.CreateTestClient(t, cfg, clientID, "Test Client No Docs Del")
|
||||
|
||||
s3Paths, err := svc.HardDelete(ctx, clientID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, s3Paths)
|
||||
|
||||
// Verify client and root folder are gone
|
||||
_, err = q.GetClient(ctx, clientID)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("deletes client with nested folder structure", func(t *testing.T) {
|
||||
clientID := "test-client-nested-del"
|
||||
test.CreateTestClient(t, cfg, clientID, "Test Client Nested Del")
|
||||
|
||||
rootFolder, err := q.GetFolderByPath(ctx, &repository.GetFolderByPathParams{
|
||||
Clientid: clientID,
|
||||
Path: "/",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create subfolder
|
||||
subFolder, err := q.CreateFolder(ctx, &repository.CreateFolderParams{
|
||||
Path: "/contracts",
|
||||
Parentid: &rootFolder.ID,
|
||||
Clientid: clientID,
|
||||
Createdby: "testuser",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create document in subfolder
|
||||
docID := createDocWithEntryForClient(t, cfg, clientID, &subFolder.ID, "nested-del-doc")
|
||||
|
||||
s3Paths, err := svc.HardDelete(ctx, clientID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, s3Paths, 1)
|
||||
|
||||
// Verify all gone
|
||||
_, err = q.GetDocumentSummary(ctx, docID)
|
||||
require.Error(t, err)
|
||||
_, err = q.GetFolderByID(ctx, subFolder.ID)
|
||||
require.Error(t, err)
|
||||
_, err = q.GetFolderByID(ctx, rootFolder.ID)
|
||||
require.Error(t, err)
|
||||
_, err = q.GetClient(ctx, clientID)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
@@ -57,12 +57,6 @@ UPDATE batch_uploads
|
||||
SET failed_filenames = failed_filenames || jsonb_build_array($2::text)
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: CancelBatchUpload :exec
|
||||
UPDATE batch_uploads
|
||||
SET status = 'cancelled',
|
||||
completed_at = NOW()
|
||||
WHERE id = $1 AND client_id = $2 AND status = 'processing';
|
||||
|
||||
-- name: GetDocumentsByBatchId :many
|
||||
SELECT id, clientId, hash
|
||||
FROM documents
|
||||
|
||||
@@ -14,6 +14,50 @@ UPDATE clients SET name = $1 WHERE clientId = $2;
|
||||
-- name: AddClientCanSync :exec
|
||||
INSERT INTO clientCanSync (canSync, clientId) VALUES ($1, $2);
|
||||
|
||||
-- name: GetAllDocumentIDsForClient :many
|
||||
-- Get all document IDs for a client, used to cascade document deletes during client delete
|
||||
SELECT id FROM documents WHERE clientId = @client_id;
|
||||
|
||||
-- name: DeleteCollectorMinCleanVersions :exec
|
||||
-- @sqlc-vet-disable
|
||||
-- Delete collector minimum clean version records for a client
|
||||
DELETE FROM collectorMinCleanVersions WHERE clientId = @client_id;
|
||||
|
||||
-- name: DeleteCollectorActiveVersions :exec
|
||||
-- @sqlc-vet-disable
|
||||
-- Delete collector active version records for a client
|
||||
DELETE FROM collectorActiveVersions WHERE clientId = @client_id;
|
||||
|
||||
-- name: DeleteCollectorVersions :exec
|
||||
-- @sqlc-vet-disable
|
||||
-- Delete collector version records for a client
|
||||
DELETE FROM collectorVersions WHERE clientId = @client_id;
|
||||
|
||||
-- name: DeleteBatchUploads :exec
|
||||
-- @sqlc-vet-disable
|
||||
-- Delete all batch upload records for a client
|
||||
DELETE FROM batch_uploads WHERE client_id = @client_id;
|
||||
|
||||
-- name: DeleteClientCanSync :exec
|
||||
-- @sqlc-vet-disable
|
||||
-- Delete clientCanSync records for a client
|
||||
DELETE FROM clientCanSync WHERE clientId = @client_id;
|
||||
|
||||
-- name: DeleteDocumentUploadsForClient :exec
|
||||
-- @sqlc-vet-disable
|
||||
-- Delete all documentUploads for a client
|
||||
DELETE FROM documentUploads WHERE clientId = @client_id;
|
||||
|
||||
-- name: DeleteAllFoldersForClient :exec
|
||||
-- @sqlc-vet-disable
|
||||
-- Delete all folders for a client. All documents must already be deleted to avoid FK violations.
|
||||
DELETE FROM folders WHERE clientId = @client_id;
|
||||
|
||||
-- name: HardDeleteClient :execrows
|
||||
-- @sqlc-vet-disable
|
||||
-- Hard delete the client row itself. Returns the number of rows deleted (0 or 1).
|
||||
DELETE FROM clients WHERE clientId = @client_id;
|
||||
|
||||
-- name: IsClientSynced :one
|
||||
-- Text extraction has been removed. A client is considered synced when
|
||||
-- all documents have completed cleaning (clean entry exists OR clean failed).
|
||||
|
||||
@@ -72,3 +72,51 @@ SELECT
|
||||
d.file_size_bytes
|
||||
FROM documents d
|
||||
WHERE d.id = $1;
|
||||
|
||||
-- name: CollectDocumentS3Paths :many
|
||||
-- @sqlc-vet-disable
|
||||
-- Collect S3 bucket+key from documentEntries before deleting, for logging orphaned S3 objects
|
||||
SELECT de.bucket, de.key FROM documentEntries de WHERE de.documentId = @document_id;
|
||||
|
||||
-- name: DeleteDocumentFieldExtractionArrayFields :exec
|
||||
-- @sqlc-vet-disable
|
||||
-- Delete array fields for all field extractions belonging to this document
|
||||
DELETE FROM documentFieldExtractionArrayFields
|
||||
WHERE fieldExtractionId IN (
|
||||
SELECT id FROM documentFieldExtractions WHERE documentId = @document_id
|
||||
);
|
||||
|
||||
-- name: DeleteDocumentFieldExtractionVersions :exec
|
||||
-- @sqlc-vet-disable
|
||||
-- Delete version entries for all field extractions belonging to this document
|
||||
DELETE FROM documentFieldExtractionVersions
|
||||
WHERE documentId = @document_id;
|
||||
|
||||
-- name: DeleteDocumentFieldExtractions :exec
|
||||
-- @sqlc-vet-disable
|
||||
-- Delete field extraction records for this document
|
||||
DELETE FROM documentFieldExtractions WHERE documentId = @document_id;
|
||||
|
||||
-- name: DeleteDocumentCleanEntries :exec
|
||||
-- @sqlc-vet-disable
|
||||
-- Delete clean entries for all cleans belonging to this document
|
||||
DELETE FROM documentCleanEntries
|
||||
WHERE cleanId IN (
|
||||
SELECT id FROM documentCleans WHERE documentId = @document_id
|
||||
);
|
||||
|
||||
-- name: DeleteDocumentCleans :exec
|
||||
-- @sqlc-vet-disable
|
||||
-- Delete clean records for this document
|
||||
DELETE FROM documentCleans WHERE documentId = @document_id;
|
||||
|
||||
-- name: DeleteDocumentEntries :exec
|
||||
-- @sqlc-vet-disable
|
||||
-- Delete S3 entry records for this document
|
||||
DELETE FROM documentEntries WHERE documentId = @document_id;
|
||||
|
||||
-- name: HardDeleteDocument :execrows
|
||||
-- @sqlc-vet-disable
|
||||
-- Hard delete the document row itself. Returns the number of rows deleted (0 or 1).
|
||||
-- documentLabels are removed automatically via ON DELETE CASCADE.
|
||||
DELETE FROM documents WHERE id = @document_id;
|
||||
|
||||
@@ -100,3 +100,56 @@ SELECT
|
||||
FROM documents d
|
||||
WHERE d.folderId = $1
|
||||
ORDER BY d.id;
|
||||
|
||||
-- name: GetFolderTreeIDs :many
|
||||
-- Get all folder IDs in a folder tree (recursive) for cascade delete operations
|
||||
WITH RECURSIVE folder_tree AS (
|
||||
SELECT id, path FROM folders WHERE id = @folder_id
|
||||
UNION ALL
|
||||
SELECT f.id, f.path FROM folders f
|
||||
JOIN folder_tree ft ON f.parentId = ft.id
|
||||
)
|
||||
SELECT id, path FROM folder_tree;
|
||||
|
||||
-- name: CountDocumentsInFolderTree :one
|
||||
-- Count documents in a folder tree to check before deletion without include_documents
|
||||
WITH RECURSIVE folder_tree AS (
|
||||
SELECT id FROM folders WHERE id = @folder_id
|
||||
UNION ALL
|
||||
SELECT f.id FROM folders f
|
||||
JOIN folder_tree ft ON f.parentId = ft.id
|
||||
)
|
||||
SELECT COUNT(*) FROM documents WHERE folderId IN (SELECT id FROM folder_tree);
|
||||
|
||||
-- name: GetDocumentIDsInFolderTree :many
|
||||
-- Get all document IDs in a folder tree for cascade deletion
|
||||
WITH RECURSIVE folder_tree AS (
|
||||
SELECT id FROM folders WHERE id = @folder_id
|
||||
UNION ALL
|
||||
SELECT f.id FROM folders f
|
||||
JOIN folder_tree ft ON f.parentId = ft.id
|
||||
)
|
||||
SELECT id FROM documents WHERE folderId IN (SELECT id FROM folder_tree);
|
||||
|
||||
-- name: DeleteDocumentUploadsInFolderTree :exec
|
||||
-- @sqlc-vet-disable
|
||||
-- Delete documentUploads rows that reference folders in the tree
|
||||
WITH RECURSIVE folder_tree AS (
|
||||
SELECT id FROM folders WHERE id = @folder_id
|
||||
UNION ALL
|
||||
SELECT f.id FROM folders f
|
||||
JOIN folder_tree ft ON f.parentId = ft.id
|
||||
)
|
||||
DELETE FROM documentUploads WHERE folder_id IN (SELECT id FROM folder_tree);
|
||||
|
||||
-- name: HardDeleteFolderTree :execrows
|
||||
-- @sqlc-vet-disable
|
||||
-- Delete all folders in the tree. PostgreSQL handles ordering within the CTE.
|
||||
-- Returns the number of rows deleted.
|
||||
WITH RECURSIVE folder_tree AS (
|
||||
SELECT id FROM folders WHERE id = @folder_id
|
||||
UNION ALL
|
||||
SELECT f.id FROM folders f
|
||||
JOIN folder_tree ft ON f.parentId = ft.id
|
||||
)
|
||||
DELETE FROM folders WHERE id IN (SELECT id FROM folder_tree);
|
||||
|
||||
@@ -33,29 +33,6 @@ func (q *Queries) AddFailedFilename(ctx context.Context, arg *AddFailedFilenameP
|
||||
return err
|
||||
}
|
||||
|
||||
const cancelBatchUpload = `-- name: CancelBatchUpload :exec
|
||||
UPDATE batch_uploads
|
||||
SET status = 'cancelled',
|
||||
completed_at = NOW()
|
||||
WHERE id = $1 AND client_id = $2 AND status = 'processing'
|
||||
`
|
||||
|
||||
type CancelBatchUploadParams struct {
|
||||
ID uuid.UUID `db:"id"`
|
||||
ClientID string `db:"client_id"`
|
||||
}
|
||||
|
||||
// CancelBatchUpload
|
||||
//
|
||||
// UPDATE batch_uploads
|
||||
// SET status = 'cancelled',
|
||||
// completed_at = NOW()
|
||||
// WHERE id = $1 AND client_id = $2 AND status = 'processing'
|
||||
func (q *Queries) CancelBatchUpload(ctx context.Context, arg *CancelBatchUploadParams) error {
|
||||
_, err := q.db.Exec(ctx, cancelBatchUpload, arg.ID, arg.ClientID)
|
||||
return err
|
||||
}
|
||||
|
||||
const countDocumentsByBatchId = `-- name: CountDocumentsByBatchId :one
|
||||
SELECT COUNT(*) as count
|
||||
FROM documents
|
||||
|
||||
@@ -264,47 +264,6 @@ func TestBatchUpload(t *testing.T) {
|
||||
assert.Contains(t, failedFilenamesStr, "document2.pdf")
|
||||
})
|
||||
|
||||
t.Run("CancelBatchUpload", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
|
||||
cfg := &serviceconfig.BaseConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
queries := cfg.GetDBQueries()
|
||||
|
||||
// Create a client
|
||||
clientID := fmt.Sprintf("TEST_CLIENT_CANCEL_%s", uuid.New().String()[:8])
|
||||
err := queries.CreateClient(ctx, &repository.CreateClientParams{
|
||||
Name: fmt.Sprintf("Test Client Cancel %s", uuid.New().String()[:8]),
|
||||
Clientid: clientID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create batch upload
|
||||
batchID, err := queries.CreateBatchUpload(ctx, &repository.CreateBatchUploadParams{
|
||||
ClientID: clientID,
|
||||
OriginalFilename: "cancel.zip",
|
||||
TotalDocuments: 10,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Cancel batch upload
|
||||
err = queries.CancelBatchUpload(ctx, &repository.CancelBatchUploadParams{
|
||||
ID: batchID,
|
||||
ClientID: clientID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify cancellation
|
||||
batch, err := queries.GetBatchUpload(ctx, &repository.GetBatchUploadParams{
|
||||
ID: batchID,
|
||||
ClientID: clientID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, repository.BatchStatusCancelled, batch.Status)
|
||||
assert.True(t, batch.CompletedAt.Valid)
|
||||
})
|
||||
|
||||
t.Run("GetDocumentsByBatchId", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -7,6 +7,8 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const addClientCanSync = `-- name: AddClientCanSync :exec
|
||||
@@ -43,6 +45,124 @@ func (q *Queries) CreateClient(ctx context.Context, arg *CreateClientParams) err
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteAllFoldersForClient = `-- name: DeleteAllFoldersForClient :exec
|
||||
DELETE FROM folders WHERE clientId = $1
|
||||
`
|
||||
|
||||
// @sqlc-vet-disable
|
||||
// Delete all folders for a client. All documents must already be deleted to avoid FK violations.
|
||||
//
|
||||
// DELETE FROM folders WHERE clientId = $1
|
||||
func (q *Queries) DeleteAllFoldersForClient(ctx context.Context, clientID string) error {
|
||||
_, err := q.db.Exec(ctx, deleteAllFoldersForClient, clientID)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteBatchUploads = `-- name: DeleteBatchUploads :exec
|
||||
DELETE FROM batch_uploads WHERE client_id = $1
|
||||
`
|
||||
|
||||
// @sqlc-vet-disable
|
||||
// Delete all batch upload records for a client
|
||||
//
|
||||
// DELETE FROM batch_uploads WHERE client_id = $1
|
||||
func (q *Queries) DeleteBatchUploads(ctx context.Context, clientID string) error {
|
||||
_, err := q.db.Exec(ctx, deleteBatchUploads, clientID)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteClientCanSync = `-- name: DeleteClientCanSync :exec
|
||||
DELETE FROM clientCanSync WHERE clientId = $1
|
||||
`
|
||||
|
||||
// @sqlc-vet-disable
|
||||
// Delete clientCanSync records for a client
|
||||
//
|
||||
// DELETE FROM clientCanSync WHERE clientId = $1
|
||||
func (q *Queries) DeleteClientCanSync(ctx context.Context, clientID string) error {
|
||||
_, err := q.db.Exec(ctx, deleteClientCanSync, clientID)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteCollectorActiveVersions = `-- name: DeleteCollectorActiveVersions :exec
|
||||
DELETE FROM collectorActiveVersions WHERE clientId = $1
|
||||
`
|
||||
|
||||
// @sqlc-vet-disable
|
||||
// Delete collector active version records for a client
|
||||
//
|
||||
// DELETE FROM collectorActiveVersions WHERE clientId = $1
|
||||
func (q *Queries) DeleteCollectorActiveVersions(ctx context.Context, clientID string) error {
|
||||
_, err := q.db.Exec(ctx, deleteCollectorActiveVersions, clientID)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteCollectorMinCleanVersions = `-- name: DeleteCollectorMinCleanVersions :exec
|
||||
DELETE FROM collectorMinCleanVersions WHERE clientId = $1
|
||||
`
|
||||
|
||||
// @sqlc-vet-disable
|
||||
// Delete collector minimum clean version records for a client
|
||||
//
|
||||
// DELETE FROM collectorMinCleanVersions WHERE clientId = $1
|
||||
func (q *Queries) DeleteCollectorMinCleanVersions(ctx context.Context, clientID string) error {
|
||||
_, err := q.db.Exec(ctx, deleteCollectorMinCleanVersions, clientID)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteCollectorVersions = `-- name: DeleteCollectorVersions :exec
|
||||
DELETE FROM collectorVersions WHERE clientId = $1
|
||||
`
|
||||
|
||||
// @sqlc-vet-disable
|
||||
// Delete collector version records for a client
|
||||
//
|
||||
// DELETE FROM collectorVersions WHERE clientId = $1
|
||||
func (q *Queries) DeleteCollectorVersions(ctx context.Context, clientID string) error {
|
||||
_, err := q.db.Exec(ctx, deleteCollectorVersions, clientID)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteDocumentUploadsForClient = `-- name: DeleteDocumentUploadsForClient :exec
|
||||
DELETE FROM documentUploads WHERE clientId = $1
|
||||
`
|
||||
|
||||
// @sqlc-vet-disable
|
||||
// Delete all documentUploads for a client
|
||||
//
|
||||
// DELETE FROM documentUploads WHERE clientId = $1
|
||||
func (q *Queries) DeleteDocumentUploadsForClient(ctx context.Context, clientID string) error {
|
||||
_, err := q.db.Exec(ctx, deleteDocumentUploadsForClient, clientID)
|
||||
return err
|
||||
}
|
||||
|
||||
const getAllDocumentIDsForClient = `-- name: GetAllDocumentIDsForClient :many
|
||||
SELECT id FROM documents WHERE clientId = $1
|
||||
`
|
||||
|
||||
// Get all document IDs for a client, used to cascade document deletes during client delete
|
||||
//
|
||||
// SELECT id FROM documents WHERE clientId = $1
|
||||
func (q *Queries) GetAllDocumentIDsForClient(ctx context.Context, clientID string) ([]uuid.UUID, error) {
|
||||
rows, err := q.db.Query(ctx, getAllDocumentIDsForClient, clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []uuid.UUID{}
|
||||
for rows.Next() {
|
||||
var id uuid.UUID
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, id)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getClient = `-- name: GetClient :one
|
||||
SELECT clientid, name, cansync FROM fullClients WHERE clientId = $1
|
||||
`
|
||||
@@ -57,6 +177,22 @@ func (q *Queries) GetClient(ctx context.Context, clientid string) (*Fullclient,
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const hardDeleteClient = `-- name: HardDeleteClient :execrows
|
||||
DELETE FROM clients WHERE clientId = $1
|
||||
`
|
||||
|
||||
// @sqlc-vet-disable
|
||||
// Hard delete the client row itself. Returns the number of rows deleted (0 or 1).
|
||||
//
|
||||
// DELETE FROM clients WHERE clientId = $1
|
||||
func (q *Queries) HardDeleteClient(ctx context.Context, clientID string) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, hardDeleteClient, clientID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const isClientSynced = `-- name: IsClientSynced :one
|
||||
WITH
|
||||
docs AS (
|
||||
|
||||
@@ -64,6 +64,39 @@ func (q *Queries) AddDocumentUpload(ctx context.Context, arg *AddDocumentUploadP
|
||||
return err
|
||||
}
|
||||
|
||||
const collectDocumentS3Paths = `-- name: CollectDocumentS3Paths :many
|
||||
SELECT de.bucket, de.key FROM documentEntries de WHERE de.documentId = $1
|
||||
`
|
||||
|
||||
type CollectDocumentS3PathsRow struct {
|
||||
Bucket string `db:"bucket"`
|
||||
Key string `db:"key"`
|
||||
}
|
||||
|
||||
// @sqlc-vet-disable
|
||||
// Collect S3 bucket+key from documentEntries before deleting, for logging orphaned S3 objects
|
||||
//
|
||||
// SELECT de.bucket, de.key FROM documentEntries de WHERE de.documentId = $1
|
||||
func (q *Queries) CollectDocumentS3Paths(ctx context.Context, documentID uuid.UUID) ([]*CollectDocumentS3PathsRow, error) {
|
||||
rows, err := q.db.Query(ctx, collectDocumentS3Paths, documentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []*CollectDocumentS3PathsRow{}
|
||||
for rows.Next() {
|
||||
var i CollectDocumentS3PathsRow
|
||||
if err := rows.Scan(&i.Bucket, &i.Key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, &i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const createDocument = `-- name: CreateDocument :one
|
||||
INSERT INTO documents (clientId, hash, batch_id, filename, folderId, originalPath, file_size_bytes) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id
|
||||
`
|
||||
@@ -96,6 +129,98 @@ func (q *Queries) CreateDocument(ctx context.Context, arg *CreateDocumentParams)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const deleteDocumentCleanEntries = `-- name: DeleteDocumentCleanEntries :exec
|
||||
DELETE FROM documentCleanEntries
|
||||
WHERE cleanId IN (
|
||||
SELECT id FROM documentCleans WHERE documentId = $1
|
||||
)
|
||||
`
|
||||
|
||||
// @sqlc-vet-disable
|
||||
// Delete clean entries for all cleans belonging to this document
|
||||
//
|
||||
// DELETE FROM documentCleanEntries
|
||||
// WHERE cleanId IN (
|
||||
// SELECT id FROM documentCleans WHERE documentId = $1
|
||||
// )
|
||||
func (q *Queries) DeleteDocumentCleanEntries(ctx context.Context, documentID uuid.UUID) error {
|
||||
_, err := q.db.Exec(ctx, deleteDocumentCleanEntries, documentID)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteDocumentCleans = `-- name: DeleteDocumentCleans :exec
|
||||
DELETE FROM documentCleans WHERE documentId = $1
|
||||
`
|
||||
|
||||
// @sqlc-vet-disable
|
||||
// Delete clean records for this document
|
||||
//
|
||||
// DELETE FROM documentCleans WHERE documentId = $1
|
||||
func (q *Queries) DeleteDocumentCleans(ctx context.Context, documentID uuid.UUID) error {
|
||||
_, err := q.db.Exec(ctx, deleteDocumentCleans, documentID)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteDocumentEntries = `-- name: DeleteDocumentEntries :exec
|
||||
DELETE FROM documentEntries WHERE documentId = $1
|
||||
`
|
||||
|
||||
// @sqlc-vet-disable
|
||||
// Delete S3 entry records for this document
|
||||
//
|
||||
// DELETE FROM documentEntries WHERE documentId = $1
|
||||
func (q *Queries) DeleteDocumentEntries(ctx context.Context, documentID uuid.UUID) error {
|
||||
_, err := q.db.Exec(ctx, deleteDocumentEntries, documentID)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteDocumentFieldExtractionArrayFields = `-- name: DeleteDocumentFieldExtractionArrayFields :exec
|
||||
DELETE FROM documentFieldExtractionArrayFields
|
||||
WHERE fieldExtractionId IN (
|
||||
SELECT id FROM documentFieldExtractions WHERE documentId = $1
|
||||
)
|
||||
`
|
||||
|
||||
// @sqlc-vet-disable
|
||||
// Delete array fields for all field extractions belonging to this document
|
||||
//
|
||||
// DELETE FROM documentFieldExtractionArrayFields
|
||||
// WHERE fieldExtractionId IN (
|
||||
// SELECT id FROM documentFieldExtractions WHERE documentId = $1
|
||||
// )
|
||||
func (q *Queries) DeleteDocumentFieldExtractionArrayFields(ctx context.Context, documentID uuid.UUID) error {
|
||||
_, err := q.db.Exec(ctx, deleteDocumentFieldExtractionArrayFields, documentID)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteDocumentFieldExtractionVersions = `-- name: DeleteDocumentFieldExtractionVersions :exec
|
||||
DELETE FROM documentFieldExtractionVersions
|
||||
WHERE documentId = $1
|
||||
`
|
||||
|
||||
// @sqlc-vet-disable
|
||||
// Delete version entries for all field extractions belonging to this document
|
||||
//
|
||||
// DELETE FROM documentFieldExtractionVersions
|
||||
// WHERE documentId = $1
|
||||
func (q *Queries) DeleteDocumentFieldExtractionVersions(ctx context.Context, documentID uuid.UUID) error {
|
||||
_, err := q.db.Exec(ctx, deleteDocumentFieldExtractionVersions, documentID)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteDocumentFieldExtractions = `-- name: DeleteDocumentFieldExtractions :exec
|
||||
DELETE FROM documentFieldExtractions WHERE documentId = $1
|
||||
`
|
||||
|
||||
// @sqlc-vet-disable
|
||||
// Delete field extraction records for this document
|
||||
//
|
||||
// DELETE FROM documentFieldExtractions WHERE documentId = $1
|
||||
func (q *Queries) DeleteDocumentFieldExtractions(ctx context.Context, documentID uuid.UUID) error {
|
||||
_, err := q.db.Exec(ctx, deleteDocumentFieldExtractions, documentID)
|
||||
return err
|
||||
}
|
||||
|
||||
const getDocumentEnriched = `-- name: GetDocumentEnriched :one
|
||||
SELECT
|
||||
d.id,
|
||||
@@ -334,6 +459,23 @@ func (q *Queries) GetDocumentUploadCurrentPart(ctx context.Context, arg *GetDocu
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const hardDeleteDocument = `-- name: HardDeleteDocument :execrows
|
||||
DELETE FROM documents WHERE id = $1
|
||||
`
|
||||
|
||||
// @sqlc-vet-disable
|
||||
// Hard delete the document row itself. Returns the number of rows deleted (0 or 1).
|
||||
// documentLabels are removed automatically via ON DELETE CASCADE.
|
||||
//
|
||||
// DELETE FROM documents WHERE id = $1
|
||||
func (q *Queries) HardDeleteDocument(ctx context.Context, documentID uuid.UUID) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, hardDeleteDocument, documentID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const listDocumentIDsBatch = `-- name: ListDocumentIDsBatch :many
|
||||
SELECT id, totalCount FROM listDocumentIDs($1, $2, $3)
|
||||
`
|
||||
|
||||
@@ -28,6 +28,32 @@ func (q *Queries) CountDocumentsByFolder(ctx context.Context, folderid *uuid.UUI
|
||||
return total, err
|
||||
}
|
||||
|
||||
const countDocumentsInFolderTree = `-- name: CountDocumentsInFolderTree :one
|
||||
WITH RECURSIVE folder_tree AS (
|
||||
SELECT id FROM folders WHERE id = $1
|
||||
UNION ALL
|
||||
SELECT f.id FROM folders f
|
||||
JOIN folder_tree ft ON f.parentId = ft.id
|
||||
)
|
||||
SELECT COUNT(*) FROM documents WHERE folderId IN (SELECT id FROM folder_tree)
|
||||
`
|
||||
|
||||
// Count documents in a folder tree to check before deletion without include_documents
|
||||
//
|
||||
// WITH RECURSIVE folder_tree AS (
|
||||
// SELECT id FROM folders WHERE id = $1
|
||||
// UNION ALL
|
||||
// SELECT f.id FROM folders f
|
||||
// JOIN folder_tree ft ON f.parentId = ft.id
|
||||
// )
|
||||
// SELECT COUNT(*) FROM documents WHERE folderId IN (SELECT id FROM folder_tree)
|
||||
func (q *Queries) CountDocumentsInFolderTree(ctx context.Context, folderID *uuid.UUID) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, countDocumentsInFolderTree, folderID)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const createFolder = `-- name: CreateFolder :one
|
||||
INSERT INTO folders (path, parentId, clientId, createdBy)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
@@ -65,6 +91,31 @@ func (q *Queries) CreateFolder(ctx context.Context, arg *CreateFolderParams) (*F
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const deleteDocumentUploadsInFolderTree = `-- name: DeleteDocumentUploadsInFolderTree :exec
|
||||
WITH RECURSIVE folder_tree AS (
|
||||
SELECT id FROM folders WHERE id = $1
|
||||
UNION ALL
|
||||
SELECT f.id FROM folders f
|
||||
JOIN folder_tree ft ON f.parentId = ft.id
|
||||
)
|
||||
DELETE FROM documentUploads WHERE folder_id IN (SELECT id FROM folder_tree)
|
||||
`
|
||||
|
||||
// @sqlc-vet-disable
|
||||
// Delete documentUploads rows that reference folders in the tree
|
||||
//
|
||||
// WITH RECURSIVE folder_tree AS (
|
||||
// SELECT id FROM folders WHERE id = $1
|
||||
// UNION ALL
|
||||
// SELECT f.id FROM folders f
|
||||
// JOIN folder_tree ft ON f.parentId = ft.id
|
||||
// )
|
||||
// DELETE FROM documentUploads WHERE folder_id IN (SELECT id FROM folder_tree)
|
||||
func (q *Queries) DeleteDocumentUploadsInFolderTree(ctx context.Context, folderID *uuid.UUID) error {
|
||||
_, err := q.db.Exec(ctx, deleteDocumentUploadsInFolderTree, folderID)
|
||||
return err
|
||||
}
|
||||
|
||||
const getClientRootFolder = `-- name: GetClientRootFolder :one
|
||||
SELECT id, path, parentid, clientid, createdat, createdby FROM folders
|
||||
WHERE clientId = $1 AND path = '/'
|
||||
@@ -88,6 +139,45 @@ func (q *Queries) GetClientRootFolder(ctx context.Context, clientid string) (*Fo
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const getDocumentIDsInFolderTree = `-- name: GetDocumentIDsInFolderTree :many
|
||||
WITH RECURSIVE folder_tree AS (
|
||||
SELECT id FROM folders WHERE id = $1
|
||||
UNION ALL
|
||||
SELECT f.id FROM folders f
|
||||
JOIN folder_tree ft ON f.parentId = ft.id
|
||||
)
|
||||
SELECT id FROM documents WHERE folderId IN (SELECT id FROM folder_tree)
|
||||
`
|
||||
|
||||
// Get all document IDs in a folder tree for cascade deletion
|
||||
//
|
||||
// WITH RECURSIVE folder_tree AS (
|
||||
// SELECT id FROM folders WHERE id = $1
|
||||
// UNION ALL
|
||||
// SELECT f.id FROM folders f
|
||||
// JOIN folder_tree ft ON f.parentId = ft.id
|
||||
// )
|
||||
// SELECT id FROM documents WHERE folderId IN (SELECT id FROM folder_tree)
|
||||
func (q *Queries) GetDocumentIDsInFolderTree(ctx context.Context, folderID *uuid.UUID) ([]uuid.UUID, error) {
|
||||
rows, err := q.db.Query(ctx, getDocumentIDsInFolderTree, folderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []uuid.UUID{}
|
||||
for rows.Next() {
|
||||
var id uuid.UUID
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, id)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getDocumentsByFolder = `-- name: GetDocumentsByFolder :many
|
||||
SELECT id, clientid, hash, batch_id, filename, folderid, originalpath, file_size_bytes FROM documents
|
||||
WHERE folderId = $1
|
||||
@@ -355,6 +445,50 @@ func (q *Queries) GetFolderTree(ctx context.Context, dollar_1 *uuid.UUID) ([]*Ge
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getFolderTreeIDs = `-- name: GetFolderTreeIDs :many
|
||||
WITH RECURSIVE folder_tree AS (
|
||||
SELECT id, path FROM folders WHERE id = $1
|
||||
UNION ALL
|
||||
SELECT f.id, f.path FROM folders f
|
||||
JOIN folder_tree ft ON f.parentId = ft.id
|
||||
)
|
||||
SELECT id, path FROM folder_tree
|
||||
`
|
||||
|
||||
type GetFolderTreeIDsRow struct {
|
||||
ID uuid.UUID `db:"id"`
|
||||
Path string `db:"path"`
|
||||
}
|
||||
|
||||
// Get all folder IDs in a folder tree (recursive) for cascade delete operations
|
||||
//
|
||||
// WITH RECURSIVE folder_tree AS (
|
||||
// SELECT id, path FROM folders WHERE id = $1
|
||||
// UNION ALL
|
||||
// SELECT f.id, f.path FROM folders f
|
||||
// JOIN folder_tree ft ON f.parentId = ft.id
|
||||
// )
|
||||
// SELECT id, path FROM folder_tree
|
||||
func (q *Queries) GetFolderTreeIDs(ctx context.Context, folderID *uuid.UUID) ([]*GetFolderTreeIDsRow, error) {
|
||||
rows, err := q.db.Query(ctx, getFolderTreeIDs, folderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []*GetFolderTreeIDsRow{}
|
||||
for rows.Next() {
|
||||
var i GetFolderTreeIDsRow
|
||||
if err := rows.Scan(&i.ID, &i.Path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, &i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getFoldersByClientID = `-- name: GetFoldersByClientID :many
|
||||
SELECT id, path, parentid, clientid, createdat, createdby FROM folders
|
||||
WHERE clientId = $1
|
||||
@@ -510,6 +644,35 @@ func (q *Queries) GetTopLevelFolders(ctx context.Context, clientid string) ([]*F
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const hardDeleteFolderTree = `-- name: HardDeleteFolderTree :execrows
|
||||
WITH RECURSIVE folder_tree AS (
|
||||
SELECT id FROM folders WHERE id = $1
|
||||
UNION ALL
|
||||
SELECT f.id FROM folders f
|
||||
JOIN folder_tree ft ON f.parentId = ft.id
|
||||
)
|
||||
DELETE FROM folders WHERE id IN (SELECT id FROM folder_tree)
|
||||
`
|
||||
|
||||
// @sqlc-vet-disable
|
||||
// Delete all folders in the tree. PostgreSQL handles ordering within the CTE.
|
||||
// Returns the number of rows deleted.
|
||||
//
|
||||
// WITH RECURSIVE folder_tree AS (
|
||||
// SELECT id FROM folders WHERE id = $1
|
||||
// UNION ALL
|
||||
// SELECT f.id FROM folders f
|
||||
// JOIN folder_tree ft ON f.parentId = ft.id
|
||||
// )
|
||||
// DELETE FROM folders WHERE id IN (SELECT id FROM folder_tree)
|
||||
func (q *Queries) HardDeleteFolderTree(ctx context.Context, folderID *uuid.UUID) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, hardDeleteFolderTree, folderID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const renameFolder = `-- name: RenameFolder :exec
|
||||
UPDATE folders
|
||||
SET path = $2
|
||||
|
||||
@@ -123,14 +123,6 @@ func (s *Service) List(ctx context.Context, clientID string, limit int32, offset
|
||||
return summaries, nil
|
||||
}
|
||||
|
||||
// Cancel cancels a batch upload in progress
|
||||
func (s *Service) Cancel(ctx context.Context, clientID string, batchID uuid.UUID) error {
|
||||
return s.cfg.GetDBQueries().CancelBatchUpload(ctx, &repository.CancelBatchUploadParams{
|
||||
ID: batchID,
|
||||
ClientID: clientID,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateProgress updates batch processing progress
|
||||
func (s *Service) UpdateProgress(ctx context.Context, clientID string, batchID uuid.UUID, processed, failed, invalidType int32) error {
|
||||
// Calculate progress percentage
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package document
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"queryorchestration/internal/database/repository"
|
||||
"queryorchestration/internal/harddelete"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// HardDelete permanently removes a document and all its dependent data from the database.
|
||||
// The delete cascade executes in FK order within a single transaction:
|
||||
// - field extraction array fields, versions, and extractions
|
||||
// - clean entries and cleans
|
||||
// - document entries (S3 references)
|
||||
// - the document row itself (documentLabels auto-cascade via ON DELETE CASCADE)
|
||||
//
|
||||
// S3 source files are intentionally NOT deleted. Their paths are collected before
|
||||
// the transaction, logged at INFO level, and returned for optional verbose responses.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: request context
|
||||
// - documentID: UUID of the document to delete
|
||||
//
|
||||
// Returns:
|
||||
// - []harddelete.S3PathInfo: S3 paths of orphaned source documents
|
||||
// - error: NotFoundError if the document does not exist, or a database error
|
||||
func (s *Service) HardDelete(ctx context.Context, documentID uuid.UUID) ([]harddelete.S3PathInfo, error) {
|
||||
queries := s.cfg.GetDBQueries()
|
||||
|
||||
// Collect S3 paths before starting the transaction so we can log them
|
||||
// even if the transaction fails for some reason.
|
||||
s3Rows, err := queries.CollectDocumentS3Paths(ctx, documentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to collect S3 paths for document %s: %w", documentID, err)
|
||||
}
|
||||
|
||||
s3Paths := make([]harddelete.S3PathInfo, len(s3Rows))
|
||||
for i, row := range s3Rows {
|
||||
s3Paths[i] = harddelete.S3PathInfo{
|
||||
DocumentID: documentID,
|
||||
Bucket: row.Bucket,
|
||||
Key: row.Key,
|
||||
}
|
||||
}
|
||||
|
||||
err = s.cfg.ExecuteDBTransaction(ctx, func(ctx context.Context, q *repository.Queries) error {
|
||||
return DeleteDocumentCascade(ctx, q, documentID)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to hard-delete document %s: %w", documentID, err)
|
||||
}
|
||||
|
||||
harddelete.LogOrphanedS3Paths(s3Paths)
|
||||
|
||||
return s3Paths, nil
|
||||
}
|
||||
|
||||
// DeleteDocumentCascade deletes a document and all dependent rows in FK order
|
||||
// using the provided transactional queries. This function does NOT collect S3 paths
|
||||
// or log them -- the caller is responsible for that.
|
||||
// Exported so that folder and client delete services can reuse the cascade logic.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: request context
|
||||
// - q: transactional repository queries
|
||||
// - documentID: UUID of the document to delete
|
||||
//
|
||||
// Returns:
|
||||
// - error: NotFoundError if the document does not exist, or a database error
|
||||
func DeleteDocumentCascade(ctx context.Context, q *repository.Queries, documentID uuid.UUID) error {
|
||||
// Delete in FK dependency order (children before parents)
|
||||
|
||||
if err := q.DeleteDocumentFieldExtractionArrayFields(ctx, documentID); err != nil {
|
||||
return fmt.Errorf("delete field extraction array fields: %w", err)
|
||||
}
|
||||
|
||||
if err := q.DeleteDocumentFieldExtractionVersions(ctx, documentID); err != nil {
|
||||
return fmt.Errorf("delete field extraction versions: %w", err)
|
||||
}
|
||||
|
||||
if err := q.DeleteDocumentFieldExtractions(ctx, documentID); err != nil {
|
||||
return fmt.Errorf("delete field extractions: %w", err)
|
||||
}
|
||||
|
||||
if err := q.DeleteDocumentCleanEntries(ctx, documentID); err != nil {
|
||||
return fmt.Errorf("delete clean entries: %w", err)
|
||||
}
|
||||
|
||||
if err := q.DeleteDocumentCleans(ctx, documentID); err != nil {
|
||||
return fmt.Errorf("delete cleans: %w", err)
|
||||
}
|
||||
|
||||
if err := q.DeleteDocumentEntries(ctx, documentID); err != nil {
|
||||
return fmt.Errorf("delete document entries: %w", err)
|
||||
}
|
||||
|
||||
// documentLabels auto-cascade via ON DELETE CASCADE, but the document
|
||||
// row itself must be explicitly deleted.
|
||||
rowsAffected, err := q.HardDeleteDocument(ctx, documentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete document: %w", err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return &harddelete.NotFoundError{
|
||||
ResourceType: "document",
|
||||
ResourceID: documentID.String(),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package document_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"queryorchestration/internal/database/repository"
|
||||
"queryorchestration/internal/document"
|
||||
"queryorchestration/internal/fieldextraction"
|
||||
"queryorchestration/internal/harddelete"
|
||||
"queryorchestration/internal/test"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// createTestDocumentWithDeps creates a document with entries, cleans, and field extractions.
|
||||
// Returns the document ID for use in delete tests.
|
||||
func createTestDocumentWithDeps(t *testing.T, cfg *TestConfig, clientID string, folderID *uuid.UUID, hash string) uuid.UUID {
|
||||
t.Helper()
|
||||
ctx := t.Context()
|
||||
q := cfg.GetDBQueries()
|
||||
|
||||
filename := hash + ".pdf"
|
||||
docID, err := q.CreateDocument(ctx, &repository.CreateDocumentParams{
|
||||
Clientid: clientID,
|
||||
Hash: hash,
|
||||
Filename: &filename,
|
||||
Folderid: folderID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add a document entry (S3 reference)
|
||||
err = q.AddDocumentEntry(ctx, &repository.AddDocumentEntryParams{
|
||||
Documentid: docID,
|
||||
Bucket: "test-bucket",
|
||||
Key: "test-key/" + hash,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add a document clean (must satisfy location_xor_fail constraint)
|
||||
cleanBucket := "clean-bucket"
|
||||
cleanKey := "clean-key/" + hash
|
||||
cleanHash := "cleanhash-" + hash
|
||||
cleanID, err := q.AddDocumentClean(ctx, &repository.AddDocumentCleanParams{
|
||||
Documentid: docID,
|
||||
Bucket: &cleanBucket,
|
||||
Key: &cleanKey,
|
||||
Hash: &cleanHash,
|
||||
Mimetype: repository.NullCleanmimetype{
|
||||
Cleanmimetype: repository.CleanmimetypeApplicationPdf,
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add a clean entry
|
||||
err = q.AddDocumentCleanEntry(ctx, &repository.AddDocumentCleanEntryParams{
|
||||
Cleanid: cleanID,
|
||||
Version: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add field extraction
|
||||
fieldSvc := fieldextraction.New(cfg)
|
||||
contractTitle := "Test Contract " + hash
|
||||
input := &fieldextraction.CreateFieldExtractionInput{
|
||||
DocumentID: docID,
|
||||
SingleFields: &repository.AddFieldExtractionParams{
|
||||
Documentid: docID,
|
||||
Filename: &filename,
|
||||
Contracttitle: &contractTitle,
|
||||
Createdby: "testuser",
|
||||
},
|
||||
ArrayFields: []*repository.AddFieldExtractionArrayFieldParams{},
|
||||
}
|
||||
_, err = fieldSvc.CreateFieldExtraction(ctx, input)
|
||||
require.NoError(t, err)
|
||||
|
||||
return docID
|
||||
}
|
||||
|
||||
func TestHardDeleteDocument(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
cfg := &TestConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
|
||||
clientID := "test-doc-delete"
|
||||
test.CreateTestClient(t, cfg, clientID, "Test Doc Delete")
|
||||
|
||||
svc := document.New(cfg)
|
||||
|
||||
t.Run("deletes document and all dependent rows", func(t *testing.T) {
|
||||
q := cfg.GetDBQueries()
|
||||
|
||||
rootFolder, err := q.GetFolderByPath(ctx, &repository.GetFolderByPathParams{
|
||||
Clientid: clientID,
|
||||
Path: "/",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
docID := createTestDocumentWithDeps(t, cfg, clientID, &rootFolder.ID, "delete-cascade-test")
|
||||
|
||||
// Verify document and deps exist before delete
|
||||
_, err = q.GetDocumentSummary(ctx, docID)
|
||||
require.NoError(t, err)
|
||||
|
||||
s3Rows, err := q.CollectDocumentS3Paths(ctx, docID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, s3Rows, 1)
|
||||
|
||||
// Delete the document
|
||||
s3Paths, err := svc.HardDelete(ctx, docID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify S3 paths returned
|
||||
assert.Len(t, s3Paths, 1)
|
||||
assert.Equal(t, "test-bucket", s3Paths[0].Bucket)
|
||||
assert.Equal(t, "test-key/delete-cascade-test", s3Paths[0].Key)
|
||||
assert.Equal(t, docID, s3Paths[0].DocumentID)
|
||||
|
||||
// Verify document is gone
|
||||
_, err = q.GetDocumentSummary(ctx, docID)
|
||||
require.Error(t, err)
|
||||
|
||||
// Verify document entries are gone
|
||||
s3Rows, err = q.CollectDocumentS3Paths(ctx, docID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, s3Rows)
|
||||
|
||||
// Verify field extractions are gone
|
||||
extractions, err := q.GetFieldExtractionsByDocumentID(ctx, docID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, extractions)
|
||||
})
|
||||
|
||||
t.Run("returns NotFoundError for non-existent document", func(t *testing.T) {
|
||||
fakeID := uuid.New()
|
||||
_, err := svc.HardDelete(ctx, fakeID)
|
||||
require.Error(t, err)
|
||||
|
||||
var notFoundErr *harddelete.NotFoundError
|
||||
assert.True(t, errors.As(err, ¬FoundErr))
|
||||
})
|
||||
|
||||
t.Run("returns empty S3 paths when document has no entries", func(t *testing.T) {
|
||||
q := cfg.GetDBQueries()
|
||||
|
||||
// Create a document with no entries
|
||||
docID, err := q.CreateDocument(ctx, &repository.CreateDocumentParams{
|
||||
Clientid: clientID,
|
||||
Hash: "no-entries-test",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
s3Paths, err := svc.HardDelete(ctx, docID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, s3Paths)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package folder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"queryorchestration/internal/database/repository"
|
||||
"queryorchestration/internal/document"
|
||||
"queryorchestration/internal/harddelete"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// HardDelete permanently removes a folder tree and optionally all documents within it.
|
||||
// The root folder (path '/') cannot be deleted and returns a ConflictError.
|
||||
//
|
||||
// If includeDocuments is false and the folder tree contains documents, a ConflictError
|
||||
// is returned. The caller must either delete the documents first or set includeDocuments
|
||||
// to true.
|
||||
//
|
||||
// When includeDocuments is true, all documents in the folder tree are cascade-deleted
|
||||
// (using the same document cascade as document.HardDelete), then documentUploads and
|
||||
// folder rows are removed. The entire operation executes in a single database transaction.
|
||||
//
|
||||
// S3 source files are intentionally NOT deleted. Their paths are collected, logged at
|
||||
// INFO level, and returned for optional verbose responses.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: request context
|
||||
// - folderID: UUID of the root folder to delete
|
||||
// - includeDocuments: if true, cascade-delete all documents in the folder tree
|
||||
//
|
||||
// Returns:
|
||||
// - []harddelete.S3PathInfo: S3 paths of orphaned source documents (empty if no documents)
|
||||
// - error: NotFoundError, ConflictError, or database error
|
||||
func (s *Service) HardDelete(ctx context.Context, folderID uuid.UUID, includeDocuments bool) ([]harddelete.S3PathInfo, error) {
|
||||
queries := s.cfg.GetDBQueries()
|
||||
|
||||
// Verify the folder exists and is not the root folder
|
||||
if err := validateFolderForDelete(ctx, queries, folderID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Gather document info and S3 paths before the transaction
|
||||
docIDs, allS3Paths, err := collectFolderDocumentInfo(ctx, queries, folderID, includeDocuments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.cfg.ExecuteDBTransaction(ctx, func(ctx context.Context, q *repository.Queries) error {
|
||||
// Delete all documents in the folder tree if requested
|
||||
for _, docID := range docIDs {
|
||||
if err := document.DeleteDocumentCascade(ctx, q, docID); err != nil {
|
||||
return fmt.Errorf("cascade delete document %s: %w", docID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete documentUploads referencing folders in the tree
|
||||
if err := q.DeleteDocumentUploadsInFolderTree(ctx, &folderID); err != nil {
|
||||
return fmt.Errorf("delete document uploads in folder tree: %w", err)
|
||||
}
|
||||
|
||||
// Delete the folder tree
|
||||
rowsAffected, err := q.HardDeleteFolderTree(ctx, &folderID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete folder tree: %w", err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return &harddelete.NotFoundError{
|
||||
ResourceType: "folder",
|
||||
ResourceID: folderID.String(),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
harddelete.LogOrphanedS3Paths(allS3Paths)
|
||||
return allS3Paths, nil
|
||||
}
|
||||
|
||||
// validateFolderForDelete checks that a folder exists and is not the root folder.
|
||||
// Returns NotFoundError or ConflictError as appropriate.
|
||||
func validateFolderForDelete(ctx context.Context, queries *repository.Queries, folderID uuid.UUID) error {
|
||||
fdr, err := queries.GetFolderByID(ctx, folderID)
|
||||
if err != nil {
|
||||
return &harddelete.NotFoundError{
|
||||
ResourceType: "folder",
|
||||
ResourceID: folderID.String(),
|
||||
}
|
||||
}
|
||||
|
||||
if fdr.Path == "/" {
|
||||
return &harddelete.ConflictError{
|
||||
Message: "cannot delete root folder",
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectFolderDocumentInfo checks for documents in the folder tree and collects their
|
||||
// S3 paths. Returns ConflictError if documents exist and includeDocuments is false.
|
||||
func collectFolderDocumentInfo(
|
||||
ctx context.Context,
|
||||
queries *repository.Queries,
|
||||
folderID uuid.UUID,
|
||||
includeDocuments bool,
|
||||
) ([]uuid.UUID, []harddelete.S3PathInfo, error) {
|
||||
docCount, err := queries.CountDocumentsInFolderTree(ctx, &folderID)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to count documents in folder tree %s: %w", folderID, err)
|
||||
}
|
||||
|
||||
if docCount > 0 && !includeDocuments {
|
||||
return nil, nil, &harddelete.ConflictError{
|
||||
Message: fmt.Sprintf("folder tree contains %d documents; set include_documents=true to delete them", docCount),
|
||||
}
|
||||
}
|
||||
|
||||
if docCount == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
docIDs, err := queries.GetDocumentIDsInFolderTree(ctx, &folderID)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get document IDs in folder tree %s: %w", folderID, err)
|
||||
}
|
||||
|
||||
allS3Paths, err := harddelete.CollectS3PathsForDocuments(ctx, docIDs, wrapS3Query(queries))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return docIDs, allS3Paths, nil
|
||||
}
|
||||
|
||||
// wrapS3Query adapts the repository's CollectDocumentS3Paths to the function signature
|
||||
// expected by harddelete.CollectS3PathsForDocuments.
|
||||
func wrapS3Query(queries *repository.Queries) func(ctx context.Context, docID uuid.UUID) ([]harddelete.S3Row, error) {
|
||||
return func(ctx context.Context, docID uuid.UUID) ([]harddelete.S3Row, error) {
|
||||
rows, err := queries.CollectDocumentS3Paths(ctx, docID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]harddelete.S3Row, len(rows))
|
||||
for i, r := range rows {
|
||||
result[i] = harddelete.S3Row{Bucket: r.Bucket, Key: r.Key}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package folder_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"queryorchestration/internal/database/repository"
|
||||
"queryorchestration/internal/folder"
|
||||
"queryorchestration/internal/harddelete"
|
||||
"queryorchestration/internal/test"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// createDocWithEntry creates a document with an S3 entry in the given folder.
|
||||
// Returns the document ID.
|
||||
func createDocWithEntry(t *testing.T, cfg *TestConfig, clientID string, folderID *uuid.UUID, hash string) uuid.UUID {
|
||||
t.Helper()
|
||||
ctx := t.Context()
|
||||
q := cfg.GetDBQueries()
|
||||
|
||||
filename := hash + ".pdf"
|
||||
docID, err := q.CreateDocument(ctx, &repository.CreateDocumentParams{
|
||||
Clientid: clientID,
|
||||
Hash: hash,
|
||||
Filename: &filename,
|
||||
Folderid: folderID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = q.AddDocumentEntry(ctx, &repository.AddDocumentEntryParams{
|
||||
Documentid: docID,
|
||||
Bucket: "test-bucket",
|
||||
Key: "test-key/" + hash,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return docID
|
||||
}
|
||||
|
||||
func TestHardDeleteFolder(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
cfg := &TestConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
|
||||
clientID := "test-folder-delete"
|
||||
test.CreateTestClient(t, cfg, clientID, "Test Folder Delete")
|
||||
|
||||
svc := folder.New(cfg)
|
||||
q := cfg.GetDBQueries()
|
||||
|
||||
// Get root folder for this client
|
||||
rootFolder, err := q.GetFolderByPath(ctx, &repository.GetFolderByPathParams{
|
||||
Clientid: clientID,
|
||||
Path: "/",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("rejects deleting root folder", func(t *testing.T) {
|
||||
_, err := svc.HardDelete(ctx, rootFolder.ID, false)
|
||||
require.Error(t, err)
|
||||
|
||||
var conflictErr *harddelete.ConflictError
|
||||
assert.True(t, errors.As(err, &conflictErr))
|
||||
assert.Contains(t, conflictErr.Message, "root folder")
|
||||
})
|
||||
|
||||
t.Run("returns NotFoundError for non-existent folder", func(t *testing.T) {
|
||||
fakeID := uuid.New()
|
||||
_, err := svc.HardDelete(ctx, fakeID, false)
|
||||
require.Error(t, err)
|
||||
|
||||
var notFoundErr *harddelete.NotFoundError
|
||||
assert.True(t, errors.As(err, ¬FoundErr))
|
||||
})
|
||||
|
||||
t.Run("deletes empty folder tree", func(t *testing.T) {
|
||||
parent, err := svc.CreateFolder(ctx, "/del-empty", &rootFolder.ID, clientID, "testuser")
|
||||
require.NoError(t, err)
|
||||
|
||||
child, err := svc.CreateFolder(ctx, "/del-empty/child", &parent.ID, clientID, "testuser")
|
||||
require.NoError(t, err)
|
||||
|
||||
s3Paths, err := svc.HardDelete(ctx, parent.ID, false)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, s3Paths)
|
||||
|
||||
// Verify both folders are gone
|
||||
_, err = q.GetFolderByID(ctx, parent.ID)
|
||||
require.Error(t, err)
|
||||
_, err = q.GetFolderByID(ctx, child.ID)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("conflicts when folder has documents and include_documents is false", func(t *testing.T) {
|
||||
fdr, err := svc.CreateFolder(ctx, "/del-has-docs", &rootFolder.ID, clientID, "testuser")
|
||||
require.NoError(t, err)
|
||||
|
||||
createDocWithEntry(t, cfg, clientID, &fdr.ID, "folder-doc-conflict")
|
||||
|
||||
_, err = svc.HardDelete(ctx, fdr.ID, false)
|
||||
require.Error(t, err)
|
||||
|
||||
var conflictErr *harddelete.ConflictError
|
||||
assert.True(t, errors.As(err, &conflictErr))
|
||||
assert.Contains(t, conflictErr.Message, "documents")
|
||||
})
|
||||
|
||||
t.Run("deletes folder tree with documents when include_documents is true", func(t *testing.T) {
|
||||
parent, err := svc.CreateFolder(ctx, "/del-with-docs", &rootFolder.ID, clientID, "testuser")
|
||||
require.NoError(t, err)
|
||||
|
||||
child, err := svc.CreateFolder(ctx, "/del-with-docs/child", &parent.ID, clientID, "testuser")
|
||||
require.NoError(t, err)
|
||||
|
||||
doc1ID := createDocWithEntry(t, cfg, clientID, &parent.ID, "parent-doc-1")
|
||||
doc2ID := createDocWithEntry(t, cfg, clientID, &child.ID, "child-doc-1")
|
||||
|
||||
s3Paths, err := svc.HardDelete(ctx, parent.ID, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify S3 paths returned for both documents
|
||||
assert.Len(t, s3Paths, 2)
|
||||
|
||||
// Verify both documents are gone
|
||||
_, err = q.GetDocumentSummary(ctx, doc1ID)
|
||||
require.Error(t, err)
|
||||
_, err = q.GetDocumentSummary(ctx, doc2ID)
|
||||
require.Error(t, err)
|
||||
|
||||
// Verify folders are gone
|
||||
_, err = q.GetFolderByID(ctx, parent.ID)
|
||||
require.Error(t, err)
|
||||
_, err = q.GetFolderByID(ctx, child.ID)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("counts documents in nested folder tree correctly", func(t *testing.T) {
|
||||
parent, err := svc.CreateFolder(ctx, "/del-count", &rootFolder.ID, clientID, "testuser")
|
||||
require.NoError(t, err)
|
||||
|
||||
child, err := svc.CreateFolder(ctx, "/del-count/sub", &parent.ID, clientID, "testuser")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Document in child, not parent
|
||||
createDocWithEntry(t, cfg, clientID, &child.ID, "nested-doc-count")
|
||||
|
||||
// Should conflict because the tree has a document (in the child)
|
||||
_, err = svc.HardDelete(ctx, parent.ID, false)
|
||||
require.Error(t, err)
|
||||
|
||||
var conflictErr *harddelete.ConflictError
|
||||
assert.True(t, errors.As(err, &conflictErr))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Package harddelete defines shared types and errors for hard-delete operations.
|
||||
// These types are used by the service layer (client, document, folder) and the
|
||||
// API handler layer to communicate delete results and errors.
|
||||
package harddelete
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// S3PathCollector is the interface for querying document S3 paths before deletion.
|
||||
// It is satisfied by both *repository.Queries and transaction-scoped queries.
|
||||
type S3PathCollector interface {
|
||||
CollectDocumentS3Paths(ctx context.Context, documentID uuid.UUID) ([]*S3Row, error)
|
||||
}
|
||||
|
||||
// S3Row holds the bucket and key from a documentEntries row.
|
||||
// This mirrors the generated CollectDocumentS3PathsRow to avoid leaking repository types.
|
||||
type S3Row struct {
|
||||
Bucket string
|
||||
Key string
|
||||
}
|
||||
|
||||
// CollectS3PathsForDocuments gathers S3 path info for a list of document IDs.
|
||||
// This is used by both client and folder hard-delete to collect orphaned S3 paths
|
||||
// before starting the delete transaction.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: request context
|
||||
// - docs: list of document UUIDs to collect paths for
|
||||
// - queryFn: function that returns S3 rows for a single document ID
|
||||
//
|
||||
// Returns the collected S3PathInfo slice, or an error if any query fails.
|
||||
func CollectS3PathsForDocuments(
|
||||
ctx context.Context,
|
||||
docs []uuid.UUID,
|
||||
queryFn func(ctx context.Context, docID uuid.UUID) ([]S3Row, error),
|
||||
) ([]S3PathInfo, error) {
|
||||
var paths []S3PathInfo
|
||||
for _, docID := range docs {
|
||||
rows, err := queryFn(ctx, docID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("collect S3 paths for document %s: %w", docID, err)
|
||||
}
|
||||
for _, row := range rows {
|
||||
paths = append(paths, S3PathInfo{
|
||||
DocumentID: docID,
|
||||
Bucket: row.Bucket,
|
||||
Key: row.Key,
|
||||
})
|
||||
}
|
||||
}
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
// LogOrphanedS3Paths logs each orphaned S3 path at INFO level for recordkeeping.
|
||||
func LogOrphanedS3Paths(paths []S3PathInfo) {
|
||||
for _, p := range paths {
|
||||
slog.Info("document deleted, S3 source orphaned",
|
||||
"documentId", p.DocumentID,
|
||||
"s3Path", fmt.Sprintf("s3://%s/%s", p.Bucket, p.Key),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// S3PathInfo represents the S3 location of an orphaned source document.
|
||||
// After a hard delete, source files in S3 are intentionally left in place.
|
||||
// This struct carries the information needed for logging and verbose responses.
|
||||
type S3PathInfo struct {
|
||||
DocumentID uuid.UUID
|
||||
Bucket string
|
||||
Key string
|
||||
}
|
||||
|
||||
// NotFoundError indicates the target resource does not exist.
|
||||
type NotFoundError struct {
|
||||
ResourceType string
|
||||
ResourceID string
|
||||
}
|
||||
|
||||
func (e *NotFoundError) Error() string {
|
||||
return fmt.Sprintf("%s not found: %s", e.ResourceType, e.ResourceID)
|
||||
}
|
||||
|
||||
// ConflictError indicates the delete cannot proceed due to a state conflict.
|
||||
// Examples: deleting root folder, folder contains documents without include_documents=true.
|
||||
type ConflictError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *ConflictError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
+585
-148
@@ -532,6 +532,18 @@ type CollectorSet struct {
|
||||
MinimumCleanerVersion *CodeVersion `json:"minimum_cleaner_version,omitempty"`
|
||||
}
|
||||
|
||||
// DeleteResponse Response for hard-delete operations when verbose=true
|
||||
type DeleteResponse struct {
|
||||
// DeletedDocuments S3 paths of orphaned source documents (only populated when verbose=true)
|
||||
DeletedDocuments []struct {
|
||||
// DocumentId ID of the deleted document
|
||||
DocumentId openapi_types.UUID `json:"documentId"`
|
||||
|
||||
// S3Path Full S3 path (s3://bucket/key) of the orphaned source file
|
||||
S3Path string `json:"s3Path"`
|
||||
} `json:"deletedDocuments"`
|
||||
}
|
||||
|
||||
// DocClient The properties of a client.
|
||||
type DocClient struct {
|
||||
// CanSync If the client is allowing active syncs
|
||||
@@ -1291,6 +1303,15 @@ type DeleteAdminUserParams struct {
|
||||
Confirm bool `form:"confirm" json:"confirm"`
|
||||
}
|
||||
|
||||
// DeleteClientParams defines parameters for DeleteClient.
|
||||
type DeleteClientParams struct {
|
||||
// Confirm Must be set to true to confirm deletion
|
||||
Confirm bool `form:"confirm" json:"confirm"`
|
||||
|
||||
// Verbose When true, returns S3 paths of orphaned source documents in the response body
|
||||
Verbose *bool `form:"verbose,omitempty" json:"verbose,omitempty"`
|
||||
}
|
||||
|
||||
// UploadDocumentMultipartBody defines parameters for UploadDocument.
|
||||
type UploadDocumentMultipartBody struct {
|
||||
// File The file to upload
|
||||
@@ -1322,6 +1343,12 @@ type ListClientFoldersParams struct {
|
||||
Metrics *bool `form:"metrics,omitempty" json:"metrics,omitempty"`
|
||||
}
|
||||
|
||||
// DeleteDocumentParams defines parameters for DeleteDocument.
|
||||
type DeleteDocumentParams struct {
|
||||
// Verbose When true, returns S3 paths of orphaned source documents in the response body
|
||||
Verbose *bool `form:"verbose,omitempty" json:"verbose,omitempty"`
|
||||
}
|
||||
|
||||
// GetDocumentParams defines parameters for GetDocument.
|
||||
type GetDocumentParams struct {
|
||||
// TextRecord When true, includes the full text extraction record in the response
|
||||
@@ -1349,6 +1376,15 @@ type GetFieldExtractionByVersionParams struct {
|
||||
Version int64 `form:"version" json:"version"`
|
||||
}
|
||||
|
||||
// DeleteFolderParams defines parameters for DeleteFolder.
|
||||
type DeleteFolderParams struct {
|
||||
// IncludeDocuments When true, also deletes all documents in the folder tree
|
||||
IncludeDocuments *bool `form:"include_documents,omitempty" json:"include_documents,omitempty"`
|
||||
|
||||
// Verbose When true, returns S3 paths of orphaned source documents in the response body
|
||||
Verbose *bool `form:"verbose,omitempty" json:"verbose,omitempty"`
|
||||
}
|
||||
|
||||
// GetFolderDocumentsParams defines parameters for GetFolderDocuments.
|
||||
type GetFolderDocumentsParams struct {
|
||||
// TextRecord When true, includes the full text extraction record for each document
|
||||
@@ -1543,6 +1579,9 @@ type ClientInterface interface {
|
||||
|
||||
CreateClient(ctx context.Context, body CreateClientJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
// DeleteClient request
|
||||
DeleteClient(ctx context.Context, id ClientID, params *DeleteClientParams, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
// GetClient request
|
||||
GetClient(ctx context.Context, id ClientID, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
@@ -1571,9 +1610,6 @@ type ClientInterface interface {
|
||||
// UploadDocumentBatchWithBody request with any body
|
||||
UploadDocumentBatchWithBody(ctx context.Context, id ClientID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
// CancelDocumentBatch request
|
||||
CancelDocumentBatch(ctx context.Context, id ClientID, batchId BatchID, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
// GetDocumentBatch request
|
||||
GetDocumentBatch(ctx context.Context, id ClientID, batchId BatchID, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
@@ -1591,6 +1627,9 @@ type ClientInterface interface {
|
||||
// ListClients request
|
||||
ListClients(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
// DeleteDocument request
|
||||
DeleteDocument(ctx context.Context, id DocumentID, params *DeleteDocumentParams, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
// GetDocument request
|
||||
GetDocument(ctx context.Context, id DocumentID, params *GetDocumentParams, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
@@ -1633,6 +1672,9 @@ type ClientInterface interface {
|
||||
|
||||
CreateFolder(ctx context.Context, body CreateFolderJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
// DeleteFolder request
|
||||
DeleteFolder(ctx context.Context, folderId openapi_types.UUID, params *DeleteFolderParams, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
// RenameFolderWithBody request with any body
|
||||
RenameFolderWithBody(ctx context.Context, folderId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
@@ -1915,6 +1957,18 @@ func (c *Client) CreateClient(ctx context.Context, body CreateClientJSONRequestB
|
||||
return c.Client.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteClient(ctx context.Context, id ClientID, params *DeleteClientParams, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||
req, err := NewDeleteClientRequest(c.Server, id, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Client.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) GetClient(ctx context.Context, id ClientID, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||
req, err := NewGetClientRequest(c.Server, id)
|
||||
if err != nil {
|
||||
@@ -2035,18 +2089,6 @@ func (c *Client) UploadDocumentBatchWithBody(ctx context.Context, id ClientID, c
|
||||
return c.Client.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) CancelDocumentBatch(ctx context.Context, id ClientID, batchId BatchID, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||
req, err := NewCancelDocumentBatchRequest(c.Server, id, batchId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Client.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) GetDocumentBatch(ctx context.Context, id ClientID, batchId BatchID, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||
req, err := NewGetDocumentBatchRequest(c.Server, id, batchId)
|
||||
if err != nil {
|
||||
@@ -2119,6 +2161,18 @@ func (c *Client) ListClients(ctx context.Context, reqEditors ...RequestEditorFn)
|
||||
return c.Client.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteDocument(ctx context.Context, id DocumentID, params *DeleteDocumentParams, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||
req, err := NewDeleteDocumentRequest(c.Server, id, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Client.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) GetDocument(ctx context.Context, id DocumentID, params *GetDocumentParams, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||
req, err := NewGetDocumentRequest(c.Server, id, params)
|
||||
if err != nil {
|
||||
@@ -2299,6 +2353,18 @@ func (c *Client) CreateFolder(ctx context.Context, body CreateFolderJSONRequestB
|
||||
return c.Client.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteFolder(ctx context.Context, folderId openapi_types.UUID, params *DeleteFolderParams, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||
req, err := NewDeleteFolderRequest(c.Server, folderId, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Client.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) RenameFolderWithBody(ctx context.Context, folderId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||
req, err := NewRenameFolderRequestWithBody(c.Server, folderId, contentType, body)
|
||||
if err != nil {
|
||||
@@ -3277,6 +3343,74 @@ func NewCreateClientRequestWithBody(server string, contentType string, body io.R
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewDeleteClientRequest generates requests for DeleteClient
|
||||
func NewDeleteClientRequest(server string, id ClientID, params *DeleteClientParams) (*http.Request, error) {
|
||||
var err error
|
||||
|
||||
var pathParam0 string
|
||||
|
||||
pathParam0, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serverURL, err := url.Parse(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
operationPath := fmt.Sprintf("/client/%s", pathParam0)
|
||||
if operationPath[0] == '/' {
|
||||
operationPath = "." + operationPath
|
||||
}
|
||||
|
||||
queryURL, err := serverURL.Parse(operationPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if params != nil {
|
||||
queryValues := queryURL.Query()
|
||||
|
||||
if queryFrag, err := runtime.StyleParamWithLocation("form", true, "confirm", runtime.ParamLocationQuery, params.Confirm); err != nil {
|
||||
return nil, err
|
||||
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
for k, v := range parsed {
|
||||
for _, v2 := range v {
|
||||
queryValues.Add(k, v2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if params.Verbose != nil {
|
||||
|
||||
if queryFrag, err := runtime.StyleParamWithLocation("form", true, "verbose", runtime.ParamLocationQuery, *params.Verbose); err != nil {
|
||||
return nil, err
|
||||
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
for k, v := range parsed {
|
||||
for _, v2 := range v {
|
||||
queryValues.Add(k, v2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
queryURL.RawQuery = queryValues.Encode()
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("DELETE", queryURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewGetClientRequest generates requests for GetClient
|
||||
func NewGetClientRequest(server string, id ClientID) (*http.Request, error) {
|
||||
var err error
|
||||
@@ -3617,47 +3751,6 @@ func NewUploadDocumentBatchRequestWithBody(server string, id ClientID, contentTy
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewCancelDocumentBatchRequest generates requests for CancelDocumentBatch
|
||||
func NewCancelDocumentBatchRequest(server string, id ClientID, batchId BatchID) (*http.Request, error) {
|
||||
var err error
|
||||
|
||||
var pathParam0 string
|
||||
|
||||
pathParam0, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pathParam1 string
|
||||
|
||||
pathParam1, err = runtime.StyleParamWithLocation("simple", false, "batch_id", runtime.ParamLocationPath, batchId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serverURL, err := url.Parse(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
operationPath := fmt.Sprintf("/client/%s/document/batch/%s", pathParam0, pathParam1)
|
||||
if operationPath[0] == '/' {
|
||||
operationPath = "." + operationPath
|
||||
}
|
||||
|
||||
queryURL, err := serverURL.Parse(operationPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("DELETE", queryURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewGetDocumentBatchRequest generates requests for GetDocumentBatch
|
||||
func NewGetDocumentBatchRequest(server string, id ClientID, batchId BatchID) (*http.Request, error) {
|
||||
var err error
|
||||
@@ -3863,6 +3956,62 @@ func NewListClientsRequest(server string) (*http.Request, error) {
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewDeleteDocumentRequest generates requests for DeleteDocument
|
||||
func NewDeleteDocumentRequest(server string, id DocumentID, params *DeleteDocumentParams) (*http.Request, error) {
|
||||
var err error
|
||||
|
||||
var pathParam0 string
|
||||
|
||||
pathParam0, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serverURL, err := url.Parse(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
operationPath := fmt.Sprintf("/document/%s", pathParam0)
|
||||
if operationPath[0] == '/' {
|
||||
operationPath = "." + operationPath
|
||||
}
|
||||
|
||||
queryURL, err := serverURL.Parse(operationPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if params != nil {
|
||||
queryValues := queryURL.Query()
|
||||
|
||||
if params.Verbose != nil {
|
||||
|
||||
if queryFrag, err := runtime.StyleParamWithLocation("form", true, "verbose", runtime.ParamLocationQuery, *params.Verbose); err != nil {
|
||||
return nil, err
|
||||
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
for k, v := range parsed {
|
||||
for _, v2 := range v {
|
||||
queryValues.Add(k, v2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
queryURL.RawQuery = queryValues.Encode()
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("DELETE", queryURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewGetDocumentRequest generates requests for GetDocument
|
||||
func NewGetDocumentRequest(server string, id DocumentID, params *GetDocumentParams) (*http.Request, error) {
|
||||
var err error
|
||||
@@ -4342,6 +4491,78 @@ func NewCreateFolderRequestWithBody(server string, contentType string, body io.R
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewDeleteFolderRequest generates requests for DeleteFolder
|
||||
func NewDeleteFolderRequest(server string, folderId openapi_types.UUID, params *DeleteFolderParams) (*http.Request, error) {
|
||||
var err error
|
||||
|
||||
var pathParam0 string
|
||||
|
||||
pathParam0, err = runtime.StyleParamWithLocation("simple", false, "folderId", runtime.ParamLocationPath, folderId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serverURL, err := url.Parse(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
operationPath := fmt.Sprintf("/folders/%s", pathParam0)
|
||||
if operationPath[0] == '/' {
|
||||
operationPath = "." + operationPath
|
||||
}
|
||||
|
||||
queryURL, err := serverURL.Parse(operationPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if params != nil {
|
||||
queryValues := queryURL.Query()
|
||||
|
||||
if params.IncludeDocuments != nil {
|
||||
|
||||
if queryFrag, err := runtime.StyleParamWithLocation("form", true, "include_documents", runtime.ParamLocationQuery, *params.IncludeDocuments); err != nil {
|
||||
return nil, err
|
||||
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
for k, v := range parsed {
|
||||
for _, v2 := range v {
|
||||
queryValues.Add(k, v2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if params.Verbose != nil {
|
||||
|
||||
if queryFrag, err := runtime.StyleParamWithLocation("form", true, "verbose", runtime.ParamLocationQuery, *params.Verbose); err != nil {
|
||||
return nil, err
|
||||
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
for k, v := range parsed {
|
||||
for _, v2 := range v {
|
||||
queryValues.Add(k, v2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
queryURL.RawQuery = queryValues.Encode()
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("DELETE", queryURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewRenameFolderRequest calls the generic RenameFolder builder with application/json body
|
||||
func NewRenameFolderRequest(server string, folderId openapi_types.UUID, body RenameFolderJSONRequestBody) (*http.Request, error) {
|
||||
var bodyReader io.Reader
|
||||
@@ -4797,6 +5018,9 @@ type ClientWithResponsesInterface interface {
|
||||
|
||||
CreateClientWithResponse(ctx context.Context, body CreateClientJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateClientResponse, error)
|
||||
|
||||
// DeleteClientWithResponse request
|
||||
DeleteClientWithResponse(ctx context.Context, id ClientID, params *DeleteClientParams, reqEditors ...RequestEditorFn) (*DeleteClientResponse, error)
|
||||
|
||||
// GetClientWithResponse request
|
||||
GetClientWithResponse(ctx context.Context, id ClientID, reqEditors ...RequestEditorFn) (*GetClientResponse, error)
|
||||
|
||||
@@ -4825,9 +5049,6 @@ type ClientWithResponsesInterface interface {
|
||||
// UploadDocumentBatchWithBodyWithResponse request with any body
|
||||
UploadDocumentBatchWithBodyWithResponse(ctx context.Context, id ClientID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UploadDocumentBatchResponse, error)
|
||||
|
||||
// CancelDocumentBatchWithResponse request
|
||||
CancelDocumentBatchWithResponse(ctx context.Context, id ClientID, batchId BatchID, reqEditors ...RequestEditorFn) (*CancelDocumentBatchResponse, error)
|
||||
|
||||
// GetDocumentBatchWithResponse request
|
||||
GetDocumentBatchWithResponse(ctx context.Context, id ClientID, batchId BatchID, reqEditors ...RequestEditorFn) (*GetDocumentBatchResponse, error)
|
||||
|
||||
@@ -4845,6 +5066,9 @@ type ClientWithResponsesInterface interface {
|
||||
// ListClientsWithResponse request
|
||||
ListClientsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListClientsResponse, error)
|
||||
|
||||
// DeleteDocumentWithResponse request
|
||||
DeleteDocumentWithResponse(ctx context.Context, id DocumentID, params *DeleteDocumentParams, reqEditors ...RequestEditorFn) (*DeleteDocumentResponse, error)
|
||||
|
||||
// GetDocumentWithResponse request
|
||||
GetDocumentWithResponse(ctx context.Context, id DocumentID, params *GetDocumentParams, reqEditors ...RequestEditorFn) (*GetDocumentResponse, error)
|
||||
|
||||
@@ -4887,6 +5111,9 @@ type ClientWithResponsesInterface interface {
|
||||
|
||||
CreateFolderWithResponse(ctx context.Context, body CreateFolderJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateFolderResponse, error)
|
||||
|
||||
// DeleteFolderWithResponse request
|
||||
DeleteFolderWithResponse(ctx context.Context, folderId openapi_types.UUID, params *DeleteFolderParams, reqEditors ...RequestEditorFn) (*DeleteFolderResponse, error)
|
||||
|
||||
// RenameFolderWithBodyWithResponse request with any body
|
||||
RenameFolderWithBodyWithResponse(ctx context.Context, folderId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RenameFolderResponse, error)
|
||||
|
||||
@@ -5362,6 +5589,33 @@ func (r CreateClientResponse) StatusCode() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
type DeleteClientResponse struct {
|
||||
Body []byte
|
||||
HTTPResponse *http.Response
|
||||
JSON200 *DeleteResponse
|
||||
JSON400 *InvalidRequest
|
||||
JSON401 *Unauthorized
|
||||
JSON404 *NotFound
|
||||
JSON429 *TooManyRequests
|
||||
JSON500 *InternalError
|
||||
}
|
||||
|
||||
// Status returns HTTPResponse.Status
|
||||
func (r DeleteClientResponse) Status() string {
|
||||
if r.HTTPResponse != nil {
|
||||
return r.HTTPResponse.Status
|
||||
}
|
||||
return http.StatusText(0)
|
||||
}
|
||||
|
||||
// StatusCode returns HTTPResponse.StatusCode
|
||||
func (r DeleteClientResponse) StatusCode() int {
|
||||
if r.HTTPResponse != nil {
|
||||
return r.HTTPResponse.StatusCode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type GetClientResponse struct {
|
||||
Body []byte
|
||||
HTTPResponse *http.Response
|
||||
@@ -5567,32 +5821,6 @@ func (r UploadDocumentBatchResponse) StatusCode() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
type CancelDocumentBatchResponse struct {
|
||||
Body []byte
|
||||
HTTPResponse *http.Response
|
||||
JSON400 *InvalidRequest
|
||||
JSON401 *Unauthorized
|
||||
JSON404 *NotFound
|
||||
JSON429 *TooManyRequests
|
||||
JSON500 *InternalError
|
||||
}
|
||||
|
||||
// Status returns HTTPResponse.Status
|
||||
func (r CancelDocumentBatchResponse) Status() string {
|
||||
if r.HTTPResponse != nil {
|
||||
return r.HTTPResponse.Status
|
||||
}
|
||||
return http.StatusText(0)
|
||||
}
|
||||
|
||||
// StatusCode returns HTTPResponse.StatusCode
|
||||
func (r CancelDocumentBatchResponse) StatusCode() int {
|
||||
if r.HTTPResponse != nil {
|
||||
return r.HTTPResponse.StatusCode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type GetDocumentBatchResponse struct {
|
||||
Body []byte
|
||||
HTTPResponse *http.Response
|
||||
@@ -5725,6 +5953,33 @@ func (r ListClientsResponse) StatusCode() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
type DeleteDocumentResponse struct {
|
||||
Body []byte
|
||||
HTTPResponse *http.Response
|
||||
JSON200 *DeleteResponse
|
||||
JSON400 *InvalidRequest
|
||||
JSON401 *Unauthorized
|
||||
JSON404 *NotFound
|
||||
JSON429 *TooManyRequests
|
||||
JSON500 *InternalError
|
||||
}
|
||||
|
||||
// Status returns HTTPResponse.Status
|
||||
func (r DeleteDocumentResponse) Status() string {
|
||||
if r.HTTPResponse != nil {
|
||||
return r.HTTPResponse.Status
|
||||
}
|
||||
return http.StatusText(0)
|
||||
}
|
||||
|
||||
// StatusCode returns HTTPResponse.StatusCode
|
||||
func (r DeleteDocumentResponse) StatusCode() int {
|
||||
if r.HTTPResponse != nil {
|
||||
return r.HTTPResponse.StatusCode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type GetDocumentResponse struct {
|
||||
Body []byte
|
||||
HTTPResponse *http.Response
|
||||
@@ -6051,6 +6306,34 @@ func (r CreateFolderResponse) StatusCode() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
type DeleteFolderResponse struct {
|
||||
Body []byte
|
||||
HTTPResponse *http.Response
|
||||
JSON200 *DeleteResponse
|
||||
JSON400 *InvalidRequest
|
||||
JSON401 *Unauthorized
|
||||
JSON404 *NotFound
|
||||
JSON409 *Conflict
|
||||
JSON429 *TooManyRequests
|
||||
JSON500 *InternalError
|
||||
}
|
||||
|
||||
// Status returns HTTPResponse.Status
|
||||
func (r DeleteFolderResponse) Status() string {
|
||||
if r.HTTPResponse != nil {
|
||||
return r.HTTPResponse.Status
|
||||
}
|
||||
return http.StatusText(0)
|
||||
}
|
||||
|
||||
// StatusCode returns HTTPResponse.StatusCode
|
||||
func (r DeleteFolderResponse) StatusCode() int {
|
||||
if r.HTTPResponse != nil {
|
||||
return r.HTTPResponse.StatusCode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type RenameFolderResponse struct {
|
||||
Body []byte
|
||||
HTTPResponse *http.Response
|
||||
@@ -6473,6 +6756,15 @@ func (c *ClientWithResponses) CreateClientWithResponse(ctx context.Context, body
|
||||
return ParseCreateClientResponse(rsp)
|
||||
}
|
||||
|
||||
// DeleteClientWithResponse request returning *DeleteClientResponse
|
||||
func (c *ClientWithResponses) DeleteClientWithResponse(ctx context.Context, id ClientID, params *DeleteClientParams, reqEditors ...RequestEditorFn) (*DeleteClientResponse, error) {
|
||||
rsp, err := c.DeleteClient(ctx, id, params, reqEditors...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ParseDeleteClientResponse(rsp)
|
||||
}
|
||||
|
||||
// GetClientWithResponse request returning *GetClientResponse
|
||||
func (c *ClientWithResponses) GetClientWithResponse(ctx context.Context, id ClientID, reqEditors ...RequestEditorFn) (*GetClientResponse, error) {
|
||||
rsp, err := c.GetClient(ctx, id, reqEditors...)
|
||||
@@ -6561,15 +6853,6 @@ func (c *ClientWithResponses) UploadDocumentBatchWithBodyWithResponse(ctx contex
|
||||
return ParseUploadDocumentBatchResponse(rsp)
|
||||
}
|
||||
|
||||
// CancelDocumentBatchWithResponse request returning *CancelDocumentBatchResponse
|
||||
func (c *ClientWithResponses) CancelDocumentBatchWithResponse(ctx context.Context, id ClientID, batchId BatchID, reqEditors ...RequestEditorFn) (*CancelDocumentBatchResponse, error) {
|
||||
rsp, err := c.CancelDocumentBatch(ctx, id, batchId, reqEditors...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ParseCancelDocumentBatchResponse(rsp)
|
||||
}
|
||||
|
||||
// GetDocumentBatchWithResponse request returning *GetDocumentBatchResponse
|
||||
func (c *ClientWithResponses) GetDocumentBatchWithResponse(ctx context.Context, id ClientID, batchId BatchID, reqEditors ...RequestEditorFn) (*GetDocumentBatchResponse, error) {
|
||||
rsp, err := c.GetDocumentBatch(ctx, id, batchId, reqEditors...)
|
||||
@@ -6623,6 +6906,15 @@ func (c *ClientWithResponses) ListClientsWithResponse(ctx context.Context, reqEd
|
||||
return ParseListClientsResponse(rsp)
|
||||
}
|
||||
|
||||
// DeleteDocumentWithResponse request returning *DeleteDocumentResponse
|
||||
func (c *ClientWithResponses) DeleteDocumentWithResponse(ctx context.Context, id DocumentID, params *DeleteDocumentParams, reqEditors ...RequestEditorFn) (*DeleteDocumentResponse, error) {
|
||||
rsp, err := c.DeleteDocument(ctx, id, params, reqEditors...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ParseDeleteDocumentResponse(rsp)
|
||||
}
|
||||
|
||||
// GetDocumentWithResponse request returning *GetDocumentResponse
|
||||
func (c *ClientWithResponses) GetDocumentWithResponse(ctx context.Context, id DocumentID, params *GetDocumentParams, reqEditors ...RequestEditorFn) (*GetDocumentResponse, error) {
|
||||
rsp, err := c.GetDocument(ctx, id, params, reqEditors...)
|
||||
@@ -6755,6 +7047,15 @@ func (c *ClientWithResponses) CreateFolderWithResponse(ctx context.Context, body
|
||||
return ParseCreateFolderResponse(rsp)
|
||||
}
|
||||
|
||||
// DeleteFolderWithResponse request returning *DeleteFolderResponse
|
||||
func (c *ClientWithResponses) DeleteFolderWithResponse(ctx context.Context, folderId openapi_types.UUID, params *DeleteFolderParams, reqEditors ...RequestEditorFn) (*DeleteFolderResponse, error) {
|
||||
rsp, err := c.DeleteFolder(ctx, folderId, params, reqEditors...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ParseDeleteFolderResponse(rsp)
|
||||
}
|
||||
|
||||
// RenameFolderWithBodyWithResponse request with arbitrary body returning *RenameFolderResponse
|
||||
func (c *ClientWithResponses) RenameFolderWithBodyWithResponse(ctx context.Context, folderId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RenameFolderResponse, error) {
|
||||
rsp, err := c.RenameFolderWithBody(ctx, folderId, contentType, body, reqEditors...)
|
||||
@@ -7911,6 +8212,67 @@ func ParseCreateClientResponse(rsp *http.Response) (*CreateClientResponse, error
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ParseDeleteClientResponse parses an HTTP response from a DeleteClientWithResponse call
|
||||
func ParseDeleteClientResponse(rsp *http.Response) (*DeleteClientResponse, error) {
|
||||
bodyBytes, err := io.ReadAll(rsp.Body)
|
||||
defer func() { _ = rsp.Body.Close() }()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &DeleteClientResponse{
|
||||
Body: bodyBytes,
|
||||
HTTPResponse: rsp,
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
|
||||
var dest DeleteResponse
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON200 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
|
||||
var dest InvalidRequest
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON400 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
|
||||
var dest Unauthorized
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON401 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404:
|
||||
var dest NotFound
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON404 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 429:
|
||||
var dest TooManyRequests
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON429 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
|
||||
var dest InternalError
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON500 = &dest
|
||||
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ParseGetClientResponse parses an HTTP response from a GetClientWithResponse call
|
||||
func ParseGetClientResponse(rsp *http.Response) (*GetClientResponse, error) {
|
||||
bodyBytes, err := io.ReadAll(rsp.Body)
|
||||
@@ -8322,60 +8684,6 @@ func ParseUploadDocumentBatchResponse(rsp *http.Response) (*UploadDocumentBatchR
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ParseCancelDocumentBatchResponse parses an HTTP response from a CancelDocumentBatchWithResponse call
|
||||
func ParseCancelDocumentBatchResponse(rsp *http.Response) (*CancelDocumentBatchResponse, error) {
|
||||
bodyBytes, err := io.ReadAll(rsp.Body)
|
||||
defer func() { _ = rsp.Body.Close() }()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &CancelDocumentBatchResponse{
|
||||
Body: bodyBytes,
|
||||
HTTPResponse: rsp,
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
|
||||
var dest InvalidRequest
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON400 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
|
||||
var dest Unauthorized
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON401 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404:
|
||||
var dest NotFound
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON404 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 429:
|
||||
var dest TooManyRequests
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON429 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
|
||||
var dest InternalError
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON500 = &dest
|
||||
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ParseGetDocumentBatchResponse parses an HTTP response from a GetDocumentBatchWithResponse call
|
||||
func ParseGetDocumentBatchResponse(rsp *http.Response) (*GetDocumentBatchResponse, error) {
|
||||
bodyBytes, err := io.ReadAll(rsp.Body)
|
||||
@@ -8660,6 +8968,67 @@ func ParseListClientsResponse(rsp *http.Response) (*ListClientsResponse, error)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ParseDeleteDocumentResponse parses an HTTP response from a DeleteDocumentWithResponse call
|
||||
func ParseDeleteDocumentResponse(rsp *http.Response) (*DeleteDocumentResponse, error) {
|
||||
bodyBytes, err := io.ReadAll(rsp.Body)
|
||||
defer func() { _ = rsp.Body.Close() }()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &DeleteDocumentResponse{
|
||||
Body: bodyBytes,
|
||||
HTTPResponse: rsp,
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
|
||||
var dest DeleteResponse
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON200 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
|
||||
var dest InvalidRequest
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON400 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
|
||||
var dest Unauthorized
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON401 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404:
|
||||
var dest NotFound
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON404 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 429:
|
||||
var dest TooManyRequests
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON429 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
|
||||
var dest InternalError
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON500 = &dest
|
||||
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ParseGetDocumentResponse parses an HTTP response from a GetDocumentWithResponse call
|
||||
func ParseGetDocumentResponse(rsp *http.Response) (*GetDocumentResponse, error) {
|
||||
bodyBytes, err := io.ReadAll(rsp.Body)
|
||||
@@ -9382,6 +9751,74 @@ func ParseCreateFolderResponse(rsp *http.Response) (*CreateFolderResponse, error
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ParseDeleteFolderResponse parses an HTTP response from a DeleteFolderWithResponse call
|
||||
func ParseDeleteFolderResponse(rsp *http.Response) (*DeleteFolderResponse, error) {
|
||||
bodyBytes, err := io.ReadAll(rsp.Body)
|
||||
defer func() { _ = rsp.Body.Close() }()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &DeleteFolderResponse{
|
||||
Body: bodyBytes,
|
||||
HTTPResponse: rsp,
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
|
||||
var dest DeleteResponse
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON200 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
|
||||
var dest InvalidRequest
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON400 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
|
||||
var dest Unauthorized
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON401 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404:
|
||||
var dest NotFound
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON404 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409:
|
||||
var dest Conflict
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON409 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 429:
|
||||
var dest TooManyRequests
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON429 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
|
||||
var dest InternalError
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON500 = &dest
|
||||
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ParseRenameFolderResponse parses an HTTP response from a RenameFolderWithResponse call
|
||||
func ParseRenameFolderResponse(rsp *http.Response) (*RenameFolderResponse, error) {
|
||||
bodyBytes, err := io.ReadAll(rsp.Body)
|
||||
|
||||
+177
-24
@@ -159,6 +159,57 @@ paths:
|
||||
$ref: "#/components/responses/TooManyRequests"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
delete:
|
||||
operationId: deleteClient
|
||||
tags:
|
||||
- ClientService
|
||||
summary: Hard-delete a client and all its data
|
||||
description: >-
|
||||
Permanently deletes a client and all associated data including documents,
|
||||
folders, collectors, batch uploads, and sync records. Source documents in
|
||||
S3 are NOT deleted but their paths are logged. This operation is
|
||||
irreversible. Requires confirm=true query parameter.
|
||||
security:
|
||||
- jwtAuth: []
|
||||
parameters:
|
||||
- name: confirm
|
||||
in: query
|
||||
description: Must be set to true to confirm deletion
|
||||
required: true
|
||||
schema:
|
||||
type: boolean
|
||||
- name: verbose
|
||||
in: query
|
||||
required: false
|
||||
description: When true, returns S3 paths of orphaned source documents in the response body
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
responses:
|
||||
"200":
|
||||
description: Client deleted successfully (verbose mode).
|
||||
headers:
|
||||
RateLimit:
|
||||
$ref: "#/components/headers/RateLimit"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/DeleteResponse"
|
||||
"204":
|
||||
description: Client deleted successfully.
|
||||
headers:
|
||||
RateLimit:
|
||||
$ref: "#/components/headers/RateLimit"
|
||||
"400":
|
||||
$ref: "#/components/responses/InvalidRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
"429":
|
||||
$ref: "#/components/responses/TooManyRequests"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
# Query paths have been removed - see remove_query_plan.md
|
||||
|
||||
@@ -505,30 +556,6 @@ paths:
|
||||
$ref: "#/components/responses/TooManyRequests"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
delete:
|
||||
operationId: cancelDocumentBatch
|
||||
tags:
|
||||
- DocumentsService
|
||||
summary: Cancel a batch upload
|
||||
description: Cancels a batch upload that is currently processing
|
||||
security:
|
||||
- jwtAuth: []
|
||||
responses:
|
||||
"204":
|
||||
description: Batch upload cancelled successfully
|
||||
headers:
|
||||
RateLimit:
|
||||
$ref: "#/components/headers/RateLimit"
|
||||
"400":
|
||||
$ref: "#/components/responses/InvalidRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
"429":
|
||||
$ref: "#/components/responses/TooManyRequests"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
/document/{id}:
|
||||
parameters:
|
||||
@@ -571,6 +598,50 @@ paths:
|
||||
$ref: "#/components/responses/TooManyRequests"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
delete:
|
||||
operationId: deleteDocument
|
||||
tags:
|
||||
- DocumentsService
|
||||
summary: Hard-delete a document
|
||||
description: >-
|
||||
Permanently deletes a document and all dependent data including entries,
|
||||
cleans, text extractions, field extractions, labels, and results. Source
|
||||
documents in S3 are NOT deleted but their paths are logged.
|
||||
security:
|
||||
- jwtAuth: []
|
||||
parameters:
|
||||
- name: verbose
|
||||
in: query
|
||||
required: false
|
||||
description: When true, returns S3 paths of orphaned source documents in the response body
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
responses:
|
||||
"200":
|
||||
description: Document deleted successfully (verbose mode).
|
||||
headers:
|
||||
RateLimit:
|
||||
$ref: "#/components/headers/RateLimit"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/DeleteResponse"
|
||||
"204":
|
||||
description: Document deleted successfully.
|
||||
headers:
|
||||
RateLimit:
|
||||
$ref: "#/components/headers/RateLimit"
|
||||
"400":
|
||||
$ref: "#/components/responses/InvalidRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
"429":
|
||||
$ref: "#/components/responses/TooManyRequests"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
/folders:
|
||||
post:
|
||||
@@ -652,6 +723,62 @@ paths:
|
||||
$ref: "#/components/responses/TooManyRequests"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
delete:
|
||||
operationId: deleteFolder
|
||||
tags:
|
||||
- FolderService
|
||||
summary: Hard-delete a folder tree
|
||||
description: >-
|
||||
Permanently deletes a folder and all its child folders recursively. The
|
||||
root folder (path '/') cannot be deleted. If the folder tree contains
|
||||
documents, include_documents=true is required or a 409 Conflict is
|
||||
returned. When include_documents=true, all documents in the tree are
|
||||
also permanently deleted. Source documents in S3 are NOT deleted but
|
||||
their paths are logged.
|
||||
security:
|
||||
- jwtAuth: []
|
||||
parameters:
|
||||
- name: include_documents
|
||||
in: query
|
||||
required: false
|
||||
description: When true, also deletes all documents in the folder tree
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
- name: verbose
|
||||
in: query
|
||||
required: false
|
||||
description: When true, returns S3 paths of orphaned source documents in the response body
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
responses:
|
||||
"200":
|
||||
description: Folder tree deleted successfully (verbose mode).
|
||||
headers:
|
||||
RateLimit:
|
||||
$ref: "#/components/headers/RateLimit"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/DeleteResponse"
|
||||
"204":
|
||||
description: Folder tree deleted successfully.
|
||||
headers:
|
||||
RateLimit:
|
||||
$ref: "#/components/headers/RateLimit"
|
||||
"400":
|
||||
$ref: "#/components/responses/InvalidRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
"409":
|
||||
$ref: "#/components/responses/Conflict"
|
||||
"429":
|
||||
$ref: "#/components/responses/TooManyRequests"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
/folders/{folderId}/documents:
|
||||
parameters:
|
||||
@@ -4817,3 +4944,29 @@ components:
|
||||
maximum: 2147483647
|
||||
description: Total number of items
|
||||
example: 150
|
||||
|
||||
DeleteResponse:
|
||||
description: Response for hard-delete operations when verbose=true
|
||||
type: object
|
||||
required:
|
||||
- deletedDocuments
|
||||
properties:
|
||||
deletedDocuments:
|
||||
type: array
|
||||
description: S3 paths of orphaned source documents (only populated when verbose=true)
|
||||
maxItems: 100000
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- documentId
|
||||
- s3Path
|
||||
properties:
|
||||
documentId:
|
||||
type: string
|
||||
format: uuid
|
||||
maxLength: 36
|
||||
description: ID of the deleted document
|
||||
s3Path:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
description: Full S3 path (s3://bucket/key) of the orphaned source file
|
||||
|
||||
Reference in New Issue
Block a user