docs: add OpenAPI documentation with Scalar UI

- Add swaggo annotations to all HTTP handlers
- Add Swagger/OpenAPI spec generation with swag
- Create separate docs server binary (drexa-docs)
- Add Makefile with build, run, and docs targets
- Configure Scalar as the API documentation UI

Run 'make docs' to regenerate, 'make run-docs' to serve.
This commit is contained in:
2025-12-13 22:44:37 +00:00
parent 918b85dfd5
commit 7b13326e22
18 changed files with 4853 additions and 59 deletions

View File

@@ -17,23 +17,39 @@ const (
DirItemKindFile = "file"
)
// DirectoryInfo represents directory metadata
// @Description Directory information including path and timestamps
type DirectoryInfo struct {
Kind string `json:"kind"`
ID string `json:"id"`
Path virtualfs.Path `json:"path,omitempty"`
Name string `json:"name"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
// Item type, always "directory"
Kind string `json:"kind" example:"directory"`
// Unique directory identifier
ID string `json:"id" example:"kRp2XYTq9A55"`
// Full path from root (included when ?include=path)
Path virtualfs.Path `json:"path,omitempty"`
// Directory name
Name string `json:"name" example:"My Documents"`
// When the directory was created (ISO 8601)
CreatedAt time.Time `json:"createdAt" example:"2024-12-13T15:04:05Z"`
// When the directory was last updated (ISO 8601)
UpdatedAt time.Time `json:"updatedAt" example:"2024-12-13T16:30:00Z"`
// When the directory was trashed, null if not trashed (ISO 8601)
DeletedAt *time.Time `json:"deletedAt,omitempty" example:"2024-12-14T10:00:00Z"`
}
// createDirectoryRequest represents a new directory creation request
// @Description Request to create a new directory
type createDirectoryRequest struct {
ParentID string `json:"parentID"`
Name string `json:"name"`
// ID of the parent directory
ParentID string `json:"parentID" example:"kRp2XYTq9A55"`
// Name for the new directory
Name string `json:"name" example:"New Folder"`
}
// postDirectoryContentRequest represents a move items request
// @Description Request to move items into this directory
type postDirectoryContentRequest struct {
Items []string `json:"items"`
// Array of file/directory IDs to move
Items []string `json:"items" example:"mElnUNCm8F22,kRp2XYTq9A55"`
}
func (h *HTTPHandler) currentDirectoryMiddleware(c *fiber.Ctx) error {
@@ -64,6 +80,21 @@ func includeParam(c *fiber.Ctx) []string {
return strings.Split(c.Query("include"), ",")
}
// createDirectory creates a new directory
// @Summary Create directory
// @Description Create a new directory within a parent directory
// @Tags directories
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param accountID path string true "Account ID" format(uuid)
// @Param request body createDirectoryRequest true "Directory details"
// @Param include query string false "Include additional fields" Enums(path)
// @Success 200 {object} DirectoryInfo "Created directory"
// @Failure 400 {object} map[string]string "Parent not found or not a directory"
// @Failure 401 {string} string "Not authenticated"
// @Failure 409 {object} map[string]string "Directory already exists"
// @Router /accounts/{accountID}/directories [post]
func (h *HTTPHandler) createDirectory(c *fiber.Ctx) error {
account := account.CurrentAccount(c)
if account == nil {
@@ -127,6 +158,19 @@ func (h *HTTPHandler) createDirectory(c *fiber.Ctx) error {
return c.JSON(i)
}
// fetchDirectory returns directory metadata
// @Summary Get directory info
// @Description Retrieve metadata for a specific directory
// @Tags directories
// @Produce json
// @Security BearerAuth
// @Param accountID path string true "Account ID" format(uuid)
// @Param directoryID path string true "Directory ID"
// @Param include query string false "Include additional fields" Enums(path)
// @Success 200 {object} DirectoryInfo "Directory metadata"
// @Failure 401 {string} string "Not authenticated"
// @Failure 404 {string} string "Directory not found"
// @Router /accounts/{accountID}/directories/{directoryID} [get]
func (h *HTTPHandler) fetchDirectory(c *fiber.Ctx) error {
node := mustCurrentDirectoryNode(c)
@@ -151,6 +195,18 @@ func (h *HTTPHandler) fetchDirectory(c *fiber.Ctx) error {
return c.JSON(i)
}
// listDirectory returns directory contents
// @Summary List directory contents
// @Description Get all files and subdirectories within a directory
// @Tags directories
// @Produce json
// @Security BearerAuth
// @Param accountID path string true "Account ID" format(uuid)
// @Param directoryID path string true "Directory ID"
// @Success 200 {array} interface{} "Array of FileInfo and DirectoryInfo objects"
// @Failure 401 {string} string "Not authenticated"
// @Failure 404 {string} string "Directory not found"
// @Router /accounts/{accountID}/directories/{directoryID}/content [get]
func (h *HTTPHandler) listDirectory(c *fiber.Ctx) error {
node := mustCurrentDirectoryNode(c)
children, err := h.vfs.ListChildren(c.Context(), h.db, node)
@@ -190,6 +246,21 @@ func (h *HTTPHandler) listDirectory(c *fiber.Ctx) error {
return c.JSON(items)
}
// patchDirectory updates directory properties
// @Summary Update directory
// @Description Update directory properties such as name (rename)
// @Tags directories
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param accountID path string true "Account ID" format(uuid)
// @Param directoryID path string true "Directory ID"
// @Param request body patchDirectoryRequest true "Directory update"
// @Success 200 {object} DirectoryInfo "Updated directory metadata"
// @Failure 400 {object} map[string]string "Invalid request"
// @Failure 401 {string} string "Not authenticated"
// @Failure 404 {string} string "Directory not found"
// @Router /accounts/{accountID}/directories/{directoryID} [patch]
func (h *HTTPHandler) patchDirectory(c *fiber.Ctx) error {
node := mustCurrentDirectoryNode(c)
@@ -229,6 +300,18 @@ func (h *HTTPHandler) patchDirectory(c *fiber.Ctx) error {
})
}
// deleteDirectory removes a directory
// @Summary Delete directory
// @Description Delete a directory permanently or move it to trash. Deleting a directory also affects all its contents.
// @Tags directories
// @Security BearerAuth
// @Param accountID path string true "Account ID" format(uuid)
// @Param directoryID path string true "Directory ID"
// @Param trash query bool false "Move to trash instead of permanent delete" default(false)
// @Success 204 {string} string "Directory deleted"
// @Failure 401 {string} string "Not authenticated"
// @Failure 404 {string} string "Directory not found"
// @Router /accounts/{accountID}/directories/{directoryID} [delete]
func (h *HTTPHandler) deleteDirectory(c *fiber.Ctx) error {
node := mustCurrentDirectoryNode(c)
@@ -259,6 +342,21 @@ func (h *HTTPHandler) deleteDirectory(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNoContent)
}
// moveItemsToDirectory moves files and directories into this directory
// @Summary Move items to directory
// @Description Move one or more files or directories into this directory. All items must currently be in the same source directory.
// @Tags directories
// @Accept json
// @Security BearerAuth
// @Param accountID path string true "Account ID" format(uuid)
// @Param directoryID path string true "Target directory ID"
// @Param request body postDirectoryContentRequest true "Items to move"
// @Success 204 {string} string "Items moved successfully"
// @Failure 400 {object} map[string]string "Invalid request or items not in same directory"
// @Failure 401 {string} string "Not authenticated"
// @Failure 404 {object} map[string]string "One or more items not found"
// @Failure 409 {object} map[string]string "Name conflict in target directory"
// @Router /accounts/{accountID}/directories/{directoryID}/content [post]
func (h *HTTPHandler) moveItemsToDirectory(c *fiber.Ctx) error {
acc := account.CurrentAccount(c)
if acc == nil {