feat: migrate to OpenAPI 3.0 with oneOf unions

- Add swagger2openapi conversion step to generate OpenAPI 3.0
- Add patch-openapi.ts script to inject oneOf discriminated unions
- Update docs server to embed static openapi.json
- Update moveItemsToDirectory response to use oneOf for items
- Add docs/README.md documenting the pipeline
- Use bun instead of node for scripts
This commit is contained in:
2025-12-14 16:43:05 +00:00
parent 7b13326e22
commit 528aa943fa
7 changed files with 2168 additions and 27 deletions

105
apps/backend/docs/README.md Normal file
View File

@@ -0,0 +1,105 @@
# API Documentation Pipeline
This document describes how API documentation is generated for the Drexa backend.
## Overview
The documentation pipeline converts Go code annotations into OpenAPI 3.0 specification that powers the Scalar API documentation UI.
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Go Code + │ │ Swagger 2.0 │ │ OpenAPI 3.0 │ │ OpenAPI 3.0 │
│ swag annotations│ ──▶ │ (swagger.json) │ ──▶ │ (converted) │ ──▶ │ (patched) │
└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │ │
swag swagger2openapi patch-openapi.ts cmd/docs/
openapi.json
```
## Why This Pipeline?
**The problem:** We want OpenAPI 3.0 features (like `oneOf` for union types) but the best annotation-based doc generator for Go (swag) only outputs Swagger 2.0.
**The solution:** A three-step pipeline:
1. Generate Swagger 2.0 with swag (keeps our existing handlers and annotations)
2. Convert to OpenAPI 3.0 with swagger2openapi
3. Patch in `oneOf` discriminated unions where needed
## Commands
### Generate documentation
```bash
make docs
```
This runs:
1. `swag init` → generates `docs/swagger.json`
2. `bunx swagger2openapi` → converts to `cmd/docs/openapi.json`
3. `bun scripts/patch-openapi.ts` → patches oneOf types
### Start documentation server
```bash
make run-docs
```
Opens Scalar UI at http://localhost:8081
## Adding New Union Types
If you add a new API that returns a union type (like `FileInfo | DirectoryInfo`), you need to update `scripts/patch-openapi.ts` to patch the schema.
Example:
```typescript
if (schemas['your_package.YourResponseType']) {
const response = schemas['your_package.YourResponseType'];
if (response.properties?.items) {
response.properties.items = {
type: 'array',
items: {
oneOf: [
{ $ref: '#/components/schemas/TypeA' },
{ $ref: '#/components/schemas/TypeB' }
],
discriminator: {
propertyName: 'kind',
mapping: {
a: '#/components/schemas/TypeA',
b: '#/components/schemas/TypeB'
}
}
}
};
}
}
```
## Files
| File | Purpose |
|------|---------|
| `docs/swagger.json` | Generated Swagger 2.0 spec (intermediate) |
| `cmd/docs/openapi.json` | Final OpenAPI 3.0 spec (embedded in docs server) |
| `scripts/patch-openapi.ts` | Patches oneOf types into the spec |
| `cmd/docs/main.go` | Scalar documentation server |
## Swag Annotations
Documentation is written as Go comments. See [swag documentation](https://github.com/swaggo/swag) for syntax.
Example:
```go
// 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"
// @Param request body createDirectoryRequest true "Directory details"
// @Success 200 {object} DirectoryInfo "Created directory"
// @Failure 400 {object} map[string]string "Bad request"
// @Router /accounts/{accountID}/directories [post]
func (h *HTTPHandler) createDirectory(c *fiber.Ctx) error {
```

View File

@@ -441,10 +441,13 @@
"BearerAuth": []
}
],
"description": "Move one or more files or directories into this directory. All items must currently be in the same source directory.",
"description": "Move one or more files or directories into this directory. Returns detailed status for each item including which were successfully moved, which had conflicts, and which encountered errors.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"directories"
],
@@ -476,10 +479,10 @@
}
],
"responses": {
"204": {
"description": "Items moved successfully",
"200": {
"description": "Move operation results with moved, conflict, and error states",
"schema": {
"type": "string"
"$ref": "#/definitions/internal_catalog.moveItemsToDirectoryResponse"
}
},
"400": {
@@ -505,15 +508,6 @@
"type": "string"
}
}
},
"409": {
"description": "Name conflict in target directory",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
@@ -1427,6 +1421,61 @@
}
}
},
"internal_catalog.moveItemError": {
"description": "Error details for a failed item move",
"type": "object",
"properties": {
"error": {
"description": "Error message describing what went wrong",
"type": "string",
"example": "permission denied"
},
"id": {
"description": "ID of the item that failed to move",
"type": "string",
"example": "mElnUNCm8F22"
}
}
},
"internal_catalog.moveItemsToDirectoryResponse": {
"description": "Response from moving items to a directory with status for each item",
"type": "object",
"properties": {
"conflicts": {
"description": "Array of IDs of items that conflicted with existing items in the target directory",
"type": "array",
"items": {
"type": "string"
},
"example": [
"xYz123AbC456"
]
},
"errors": {
"description": "Array of errors that occurred during the move operation",
"type": "array",
"items": {
"$ref": "#/definitions/internal_catalog.moveItemError"
}
},
"items": {
"description": "Array of items included in the request (FileInfo or DirectoryInfo objects)",
"type": "array",
"items": {}
},
"moved": {
"description": "Array of IDs of successfully moved items",
"type": "array",
"items": {
"type": "string"
},
"example": [
"mElnUNCm8F22",
"kRp2XYTq9A55"
]
}
}
},
"internal_catalog.patchDirectoryRequest": {
"description": "Request to update directory properties",
"type": "object",