mirror of
https://github.com/bretzel-app/crumbs.git
synced 2026-10-12 12:06:57 +02:00
docs: deduplicate README, add OpenAPI spec with auto-generated API docs
- Slim README to essentials, link to docs/ for details - Add docs/openapi.yaml as single source of truth for the API - Add scripts/generate-api-docs.ts to produce docs/API.md from the spec - Add pnpm docs:api / make docs-api target Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
66c91df983
commit
26cae27857
@@ -60,6 +60,7 @@ A `Makefile` wraps all common tasks for tool-agnostic usage. Run `make help` to
|
||||
| `make db-generate` | `pnpm db:generate` | Generate migrations |
|
||||
| `make db-migrate` | `pnpm db:migrate` | Run migrations |
|
||||
| `make db-studio` | `pnpm db:studio` | Open Drizzle Studio |
|
||||
| `make docs-api` | `pnpm docs:api` | Regenerate API docs from OpenAPI spec |
|
||||
| `make install` | `pnpm install` | Install dependencies |
|
||||
| `make docker-build` | — | Build Docker image |
|
||||
| `make docker-up` | — | Start containers |
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.PHONY: help dev build preview check test test-unit test-e2e lint \
|
||||
db-push db-generate db-migrate db-studio \
|
||||
db-push db-generate db-migrate db-studio docs-api \
|
||||
install docker-build docker-up docker-down docker-logs clean
|
||||
|
||||
help: ## List all targets
|
||||
@@ -41,6 +41,9 @@ db-migrate: ## Run migrations
|
||||
db-studio: ## Open Drizzle Studio
|
||||
pnpm db:studio
|
||||
|
||||
docs-api: ## Regenerate API docs from OpenAPI spec
|
||||
pnpm docs:api
|
||||
|
||||
install: ## Install dependencies
|
||||
pnpm install
|
||||
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
# Crumbs by Bretzel
|
||||
# 🥨 Crumbs by Bretzel
|
||||
|
||||
A self-hostable, offline-first note-taking app inspired by Google Keep. Part of the [Bretzel](https://bretzel.app) app universe. Built with SvelteKit, SQLite, and Tailwind CSS.
|
||||
|
||||
## Features
|
||||
|
||||
- **Notes CRUD** - Create, edit, and delete notes with rich Markdown support
|
||||
- **Markdown** - Bold, italic, headings, lists, code blocks, tables, links
|
||||
- **Checklists** - Interactive task lists with checkboxes
|
||||
- **Image Attachments** - Upload and display images in notes
|
||||
- **Tags** - Organize notes with inline #hashtags
|
||||
- **Colors** - 12 color-coded note backgrounds (matching Google Keep)
|
||||
- **Pin** - Pin important notes to the top
|
||||
- **Archive** - Archive notes to declutter your main view
|
||||
- **Trash** - Soft delete with restore capability
|
||||
- **Search** - Full-text search across titles, content, and tags
|
||||
- **Dark Mode** - System-aware with manual toggle
|
||||
- **Single-User Auth** - Password-protected access
|
||||
- **PWA** - Installable as a standalone app with offline support
|
||||
- **Local-First** - Notes stored in IndexedDB, synced to server via LWW CRDTs
|
||||
- **Docker** - Single command deployment with `docker compose up`
|
||||
A self-hostable, offline-first note-taking app inspired by Google Keep. Part of the [Bretzel](https://bretzel.app) app universe.
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -33,118 +15,60 @@ Open http://localhost:3000 and set your password on first visit.
|
||||
### Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Start dev server
|
||||
pnpm dev
|
||||
|
||||
# Run unit tests
|
||||
pnpm test:unit
|
||||
|
||||
# Run E2E tests (requires Playwright browsers)
|
||||
npx playwright install chromium
|
||||
pnpm test:e2e
|
||||
|
||||
# Type check
|
||||
pnpm check
|
||||
|
||||
# Build for production
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Run `make help` to see all available commands, or use pnpm directly:
|
||||
|
||||
```bash
|
||||
pnpm test # Unit + E2E tests
|
||||
pnpm check # Type checking
|
||||
pnpm build # Production build
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Rich notes with Markdown, checklists, image attachments, and 12 color themes
|
||||
- Organize with #tags, pinning, archive, and trash
|
||||
- Full-text search across titles, content, and tags
|
||||
- Dark mode with system detection
|
||||
- PWA — installable, works offline via IndexedDB + LWW CRDT sync
|
||||
- Single-user password auth (Argon2)
|
||||
- Docker deployment with a single command
|
||||
|
||||
See [docs/FEATURES.md](docs/FEATURES.md) for detailed feature documentation.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| Framework | SvelteKit 2 |
|
||||
| Framework | SvelteKit 2 (Svelte 5 runes) |
|
||||
| Language | TypeScript (strict) |
|
||||
| UI | Tailwind CSS 4 |
|
||||
| Database | SQLite (better-sqlite3) |
|
||||
| ORM | Drizzle ORM |
|
||||
| Database | SQLite (better-sqlite3) + Drizzle ORM |
|
||||
| Client DB | IndexedDB (idb) |
|
||||
| Sync | LWW CRDTs |
|
||||
| Auth | Argon2 + session cookies |
|
||||
| Markdown | markdown-it |
|
||||
| PWA | @vite-pwa/sveltekit |
|
||||
| Testing | Vitest + Playwright |
|
||||
| Container | Docker (multi-stage) |
|
||||
| CI/CD | GitHub Actions |
|
||||
|
||||
## Project Structure
|
||||
## Documentation
|
||||
|
||||
```
|
||||
src/
|
||||
├── lib/
|
||||
│ ├── components/ # Svelte UI components
|
||||
│ ├── server/db/ # Drizzle schema & connection
|
||||
│ ├── server/auth.ts # Authentication logic
|
||||
│ ├── sync/ # CRDT sync engine (client + server)
|
||||
│ ├── stores/ # Svelte reactive stores
|
||||
│ ├── types/ # TypeScript type definitions
|
||||
│ └── utils/ # Markdown, tags, colors, debounce
|
||||
├── routes/
|
||||
│ ├── api/ # REST API endpoints
|
||||
│ ├── login/ # Login page
|
||||
│ ├── setup/ # First-run setup page
|
||||
│ └── +page.svelte # Main notes view
|
||||
└── hooks.server.ts # Auth middleware
|
||||
tests/
|
||||
├── unit/ # Vitest unit tests
|
||||
└── e2e/ # Playwright E2E tests
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/notes?filter=all\|archived\|trashed` | List notes |
|
||||
| POST | `/api/notes` | Create a note |
|
||||
| GET | `/api/notes/:id` | Get a note |
|
||||
| PATCH | `/api/notes/:id` | Update a note |
|
||||
| DELETE | `/api/notes/:id` | Delete a note permanently |
|
||||
| GET | `/api/search?q=query` | Full-text search |
|
||||
| GET | `/api/tags` | List all tags |
|
||||
| POST | `/api/sync` | Push sync changes |
|
||||
| GET | `/api/sync?since=timestamp` | Pull changes since timestamp |
|
||||
| POST | `/api/auth/setup` | First-time password setup |
|
||||
| POST | `/api/auth/login` | Login |
|
||||
| POST | `/api/auth/logout` | Logout |
|
||||
| GET/POST/DELETE | `/api/notes/:id/attachments` | Manage image attachments |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `DATABASE_URL` | `./data/crumbs.db` | SQLite database file path |
|
||||
| `DATA_DIR` | `./data` | Data directory for attachments |
|
||||
| `ORIGIN` | `http://localhost:3000` | Server origin (CSRF protection) |
|
||||
| `PORT` | `3000` | Server port |
|
||||
| `NODE_ENV` | `development` | Node environment |
|
||||
| Document | Description |
|
||||
|----------|-------------|
|
||||
| [Features](docs/FEATURES.md) | Detailed feature list and behavior |
|
||||
| [Architecture](docs/ARCHITECTURE.md) | Local-first sync design, tech rationale, DB schema |
|
||||
| [Deployment](docs/DEPLOYMENT.md) | Docker, Node.js, reverse proxy, backups, env vars |
|
||||
| [API](docs/API.md) | REST API reference (auto-generated) |
|
||||
|
||||
## CI/CD
|
||||
|
||||
GitHub Actions workflows are included:
|
||||
- **CI** — lint, type check, unit tests, E2E tests, Docker build on every push/PR
|
||||
- **Release** — builds and pushes Docker image on `v*` tags
|
||||
|
||||
- **CI** (`ci.yml`): Runs on every push/PR - lint, type check, unit tests, build, E2E tests, Docker build
|
||||
- **Release** (`release.yml`): Runs on version tags (`v*`) - builds and pushes Docker image to a configurable registry
|
||||
|
||||
Configure your registry via GitHub Secrets:
|
||||
- `REGISTRY_URL` - Container registry URL
|
||||
- `REGISTRY_USER` - Registry username
|
||||
- `REGISTRY_TOKEN` - Registry access token
|
||||
|
||||
## Sync Architecture
|
||||
|
||||
Crumbs uses a local-first architecture with Last-Write-Wins (LWW) conflict resolution:
|
||||
|
||||
1. All reads come from IndexedDB (instant, works offline)
|
||||
2. Writes go to IndexedDB + a sync queue
|
||||
3. Background sync pushes queued changes to server every 30s
|
||||
4. Server resolves conflicts using timestamps (newer wins)
|
||||
5. Client pulls server changes and merges with local state
|
||||
|
||||
This ensures the app works fully offline and syncs automatically when connectivity is restored.
|
||||
Configure registry via GitHub Secrets: `REGISTRY_URL`, `REGISTRY_USER`, `REGISTRY_TOKEN`.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+397
-87
@@ -1,166 +1,476 @@
|
||||
# API Reference
|
||||
<!-- AUTO-GENERATED from docs/openapi.yaml — do not edit manually -->
|
||||
<!-- Run: pnpm docs:api -->
|
||||
|
||||
All API endpoints require authentication (session cookie) except `/api/auth/setup` and `/api/auth/login`.
|
||||
# Crumbs API
|
||||
|
||||
## Authentication
|
||||
REST API for Crumbs by Bretzel — a self-hostable, offline-first notes app.
|
||||
|
||||
### POST /api/auth/setup
|
||||
First-time setup. Creates the single user account.
|
||||
Base URL: `http://localhost:3000`
|
||||
|
||||
All endpoints require a `session` cookie (set by login) except where noted.
|
||||
|
||||
## Auth
|
||||
|
||||
### `POST /api/auth/setup`
|
||||
|
||||
First-time password setup
|
||||
|
||||
Creates the single user account. Only works once — returns 400 if already set up.
|
||||
|
||||
> No authentication required.
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{ "password": "min8characters" }
|
||||
{
|
||||
"password": "password"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `201 Created`
|
||||
**Response:** `201`
|
||||
|
||||
```json
|
||||
{ "success": true }
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
Sets `session` cookie.
|
||||
|
||||
**Errors:** `400` Setup already completed or invalid password
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/auth/login`
|
||||
|
||||
Log in
|
||||
|
||||
> No authentication required.
|
||||
|
||||
### POST /api/auth/login
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{ "password": "yourpassword" }
|
||||
{
|
||||
"password": "password"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `200 OK`
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
Sets `session` cookie.
|
||||
**Response:** `200`
|
||||
|
||||
### POST /api/auth/logout
|
||||
Clears session.
|
||||
|
||||
**Response:** `200 OK`
|
||||
```json
|
||||
{ "success": true }
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:** `401` Invalid password
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/auth/logout`
|
||||
|
||||
Log out
|
||||
|
||||
**Response:** `200`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
### GET /api/notes
|
||||
List notes by filter.
|
||||
### `GET /api/notes`
|
||||
|
||||
**Query Parameters:**
|
||||
- `filter` - `all` (default), `archived`, `trashed`
|
||||
List notes
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|----|------|----------|-------------|
|
||||
| `filter` | query | string | no | (all, archived, trashed) — default: `all` |
|
||||
|
||||
**Response:** `200`
|
||||
|
||||
**Response:** `200 OK`
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid",
|
||||
"title": "My Note",
|
||||
"content": "**Markdown** content",
|
||||
"color": "coral",
|
||||
"title": "title",
|
||||
"content": "content",
|
||||
"color": "default",
|
||||
"pinned": true,
|
||||
"archived": false,
|
||||
"trashed": false,
|
||||
"checklistMode": false,
|
||||
"sortOrder": 0,
|
||||
"createdAt": "2024-01-01T00:00:00.000Z",
|
||||
"updatedAt": "2024-01-01T00:00:00.000Z",
|
||||
"archived": true,
|
||||
"trashed": true,
|
||||
"checklistMode": true,
|
||||
"sortOrder": 1,
|
||||
"createdAt": "2025-01-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-01-01T00:00:00.000Z",
|
||||
"version": 1,
|
||||
"tags": ["work", "important"]
|
||||
"tags": [
|
||||
"item"
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### POST /api/notes
|
||||
Create a new note.
|
||||
---
|
||||
|
||||
### `POST /api/notes`
|
||||
|
||||
Create a note
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "My Note",
|
||||
"content": "Note content with #tags",
|
||||
"title": "title",
|
||||
"content": "content",
|
||||
"color": "default",
|
||||
"pinned": false,
|
||||
"checklistMode": false
|
||||
"pinned": true,
|
||||
"archived": true,
|
||||
"trashed": true,
|
||||
"checklistMode": true,
|
||||
"sortOrder": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `201 Created` - Returns the created note.
|
||||
**Response:** `201`
|
||||
|
||||
### GET /api/notes/:id
|
||||
Get a single note.
|
||||
|
||||
### PATCH /api/notes/:id
|
||||
Update a note.
|
||||
|
||||
**Request:** (partial update - only include changed fields)
|
||||
```json
|
||||
{
|
||||
"title": "Updated Title",
|
||||
"color": "coral",
|
||||
"pinned": true
|
||||
"id": "uuid",
|
||||
"title": "title",
|
||||
"content": "content",
|
||||
"color": "default",
|
||||
"pinned": true,
|
||||
"archived": true,
|
||||
"trashed": true,
|
||||
"checklistMode": true,
|
||||
"sortOrder": 1,
|
||||
"createdAt": "2025-01-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-01-01T00:00:00.000Z",
|
||||
"version": 1,
|
||||
"tags": [
|
||||
"item"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `200 OK` - Returns the updated note.
|
||||
---
|
||||
|
||||
### DELETE /api/notes/:id
|
||||
Permanently delete a note.
|
||||
### `GET /api/notes/{id}`
|
||||
|
||||
## Search
|
||||
Get a note
|
||||
|
||||
### GET /api/search
|
||||
Full-text search across notes.
|
||||
**Parameters:**
|
||||
|
||||
**Query Parameters:**
|
||||
- `q` - Search query
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|----|------|----------|-------------|
|
||||
| `id` | path | string | yes | |
|
||||
|
||||
**Response:** `200 OK` - Array of matching notes (excluding trashed).
|
||||
**Response:** `200`
|
||||
|
||||
## Tags
|
||||
|
||||
### GET /api/tags
|
||||
List all tags.
|
||||
|
||||
**Response:** `200 OK`
|
||||
```json
|
||||
[
|
||||
{ "id": 1, "name": "work" },
|
||||
{ "id": 2, "name": "personal" }
|
||||
]
|
||||
{
|
||||
"id": "uuid",
|
||||
"title": "title",
|
||||
"content": "content",
|
||||
"color": "default",
|
||||
"pinned": true,
|
||||
"archived": true,
|
||||
"trashed": true,
|
||||
"checklistMode": true,
|
||||
"sortOrder": 1,
|
||||
"createdAt": "2025-01-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-01-01T00:00:00.000Z",
|
||||
"version": 1,
|
||||
"tags": [
|
||||
"item"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:** `404` Note not found
|
||||
|
||||
---
|
||||
|
||||
### `PATCH /api/notes/{id}`
|
||||
|
||||
Update a note
|
||||
|
||||
Partial update — only include changed fields.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|----|------|----------|-------------|
|
||||
| `id` | path | string | yes | |
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "title",
|
||||
"content": "content",
|
||||
"color": "default",
|
||||
"pinned": true,
|
||||
"archived": true,
|
||||
"trashed": true,
|
||||
"checklistMode": true,
|
||||
"sortOrder": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `200`
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"title": "title",
|
||||
"content": "content",
|
||||
"color": "default",
|
||||
"pinned": true,
|
||||
"archived": true,
|
||||
"trashed": true,
|
||||
"checklistMode": true,
|
||||
"sortOrder": 1,
|
||||
"createdAt": "2025-01-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-01-01T00:00:00.000Z",
|
||||
"version": 1,
|
||||
"tags": [
|
||||
"item"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:** `404` Note not found
|
||||
|
||||
---
|
||||
|
||||
### `DELETE /api/notes/{id}`
|
||||
|
||||
Permanently delete a note
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|----|------|----------|-------------|
|
||||
| `id` | path | string | yes | |
|
||||
|
||||
**Response:** `200`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:** `404` Note not found
|
||||
|
||||
---
|
||||
|
||||
## Attachments
|
||||
|
||||
### GET /api/notes/:id/attachments
|
||||
List attachments for a note.
|
||||
### `GET /api/notes/{id}/attachments`
|
||||
|
||||
### POST /api/notes/:id/attachments
|
||||
Upload an image attachment.
|
||||
List or download attachments
|
||||
|
||||
Without `attachmentId`, lists all attachments. With `attachmentId`, returns the file.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|----|------|----------|-------------|
|
||||
| `id` | path | string | yes | |
|
||||
| `attachmentId` | query | string | no | |
|
||||
|
||||
**Response:** `200`
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/notes/{id}/attachments`
|
||||
|
||||
Upload an image attachment
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|----|------|----------|-------------|
|
||||
| `id` | path | string | yes | |
|
||||
|
||||
**Request:** `multipart/form-data` with `file` field.
|
||||
|
||||
**Response:** `201 Created` - Returns attachment metadata.
|
||||
**Response:** `201`
|
||||
|
||||
### DELETE /api/notes/:id/attachments?attachmentId=xxx
|
||||
Delete an attachment.
|
||||
```json
|
||||
{
|
||||
"id": "id",
|
||||
"noteId": "noteId",
|
||||
"filename": "filename",
|
||||
"mimeType": "mimeType",
|
||||
"size": 1,
|
||||
"createdAt": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:** `400` Invalid file
|
||||
|
||||
---
|
||||
|
||||
### `DELETE /api/notes/{id}/attachments`
|
||||
|
||||
Delete an attachment
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|----|------|----------|-------------|
|
||||
| `id` | path | string | yes | |
|
||||
| `attachmentId` | query | string | yes | |
|
||||
|
||||
**Response:** `200`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Search
|
||||
|
||||
### `GET /api/search`
|
||||
|
||||
Full-text search
|
||||
|
||||
Searches across note titles, content, and tags. Excludes trashed notes.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|----|------|----------|-------------|
|
||||
| `q` | query | string | yes | |
|
||||
|
||||
**Response:** `200`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid",
|
||||
"title": "title",
|
||||
"content": "content",
|
||||
"color": "default",
|
||||
"pinned": true,
|
||||
"archived": true,
|
||||
"trashed": true,
|
||||
"checklistMode": true,
|
||||
"sortOrder": 1,
|
||||
"createdAt": "2025-01-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-01-01T00:00:00.000Z",
|
||||
"version": 1,
|
||||
"tags": [
|
||||
"item"
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tags
|
||||
|
||||
### `GET /api/tags`
|
||||
|
||||
List all tags
|
||||
|
||||
**Response:** `200`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "name"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sync
|
||||
|
||||
### POST /api/sync
|
||||
Push local changes to server.
|
||||
### `GET /api/sync`
|
||||
|
||||
Pull changes since timestamp
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|----|------|----------|-------------|
|
||||
| `since` | query | integer | yes | Unix timestamp in milliseconds |
|
||||
|
||||
**Response:** `200`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid",
|
||||
"title": "title",
|
||||
"content": "content",
|
||||
"color": "default",
|
||||
"pinned": true,
|
||||
"archived": true,
|
||||
"trashed": true,
|
||||
"checklistMode": true,
|
||||
"sortOrder": 1,
|
||||
"createdAt": "2025-01-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-01-01T00:00:00.000Z",
|
||||
"version": 1,
|
||||
"tags": [
|
||||
"item"
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/sync`
|
||||
|
||||
Push local changes
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{
|
||||
"changes": [
|
||||
{
|
||||
"noteId": "uuid",
|
||||
"operation": "create|update|delete",
|
||||
"timestamp": 1704067200000,
|
||||
"data": { "title": "...", "content": "..." }
|
||||
"operation": "create",
|
||||
"timestamp": 1,
|
||||
"data": {
|
||||
"title": "title",
|
||||
"content": "content",
|
||||
"color": "default",
|
||||
"pinned": true,
|
||||
"archived": true,
|
||||
"trashed": true,
|
||||
"checklistMode": true,
|
||||
"sortOrder": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/sync?since=timestamp
|
||||
Pull changes since a timestamp (milliseconds).
|
||||
**Response:** `200`
|
||||
|
||||
**Response:** `200 OK` - Array of notes updated since the given timestamp.
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Crumbs API
|
||||
description: REST API for Crumbs by Bretzel — a self-hostable, offline-first notes app.
|
||||
version: 0.1.0
|
||||
contact:
|
||||
name: Bretzel
|
||||
url: https://bretzel.app
|
||||
|
||||
servers:
|
||||
- url: http://localhost:3000
|
||||
description: Local development
|
||||
|
||||
security:
|
||||
- sessionCookie: []
|
||||
|
||||
paths:
|
||||
/api/auth/setup:
|
||||
post:
|
||||
summary: First-time password setup
|
||||
description: Creates the single user account. Only works once — returns 400 if already set up.
|
||||
tags: [Auth]
|
||||
security: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [password]
|
||||
properties:
|
||||
password:
|
||||
type: string
|
||||
minLength: 8
|
||||
responses:
|
||||
'201':
|
||||
description: Account created, session cookie set
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Success'
|
||||
'400':
|
||||
description: Setup already completed or invalid password
|
||||
|
||||
/api/auth/login:
|
||||
post:
|
||||
summary: Log in
|
||||
tags: [Auth]
|
||||
security: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [password]
|
||||
properties:
|
||||
password:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Logged in, session cookie set
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Success'
|
||||
'401':
|
||||
description: Invalid password
|
||||
|
||||
/api/auth/logout:
|
||||
post:
|
||||
summary: Log out
|
||||
tags: [Auth]
|
||||
responses:
|
||||
'200':
|
||||
description: Session cleared
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Success'
|
||||
|
||||
/api/notes:
|
||||
get:
|
||||
summary: List notes
|
||||
tags: [Notes]
|
||||
parameters:
|
||||
- name: filter
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum: [all, archived, trashed]
|
||||
default: all
|
||||
responses:
|
||||
'200':
|
||||
description: Array of notes
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Note'
|
||||
post:
|
||||
summary: Create a note
|
||||
tags: [Notes]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/NoteInput'
|
||||
responses:
|
||||
'201':
|
||||
description: Created note
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Note'
|
||||
|
||||
/api/notes/{id}:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
get:
|
||||
summary: Get a note
|
||||
tags: [Notes]
|
||||
responses:
|
||||
'200':
|
||||
description: Note with tags
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Note'
|
||||
'404':
|
||||
description: Note not found
|
||||
patch:
|
||||
summary: Update a note
|
||||
description: Partial update — only include changed fields.
|
||||
tags: [Notes]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/NoteInput'
|
||||
responses:
|
||||
'200':
|
||||
description: Updated note
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Note'
|
||||
'404':
|
||||
description: Note not found
|
||||
delete:
|
||||
summary: Permanently delete a note
|
||||
tags: [Notes]
|
||||
responses:
|
||||
'200':
|
||||
description: Deleted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Success'
|
||||
'404':
|
||||
description: Note not found
|
||||
|
||||
/api/notes/{id}/attachments:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
get:
|
||||
summary: List or download attachments
|
||||
description: Without `attachmentId`, lists all attachments. With `attachmentId`, returns the file.
|
||||
tags: [Attachments]
|
||||
parameters:
|
||||
- name: attachmentId
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Attachment list or file content
|
||||
post:
|
||||
summary: Upload an image attachment
|
||||
tags: [Attachments]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
required: [file]
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
description: Image file (max 10MB)
|
||||
responses:
|
||||
'201':
|
||||
description: Attachment metadata
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Attachment'
|
||||
'400':
|
||||
description: Invalid file
|
||||
delete:
|
||||
summary: Delete an attachment
|
||||
tags: [Attachments]
|
||||
parameters:
|
||||
- name: attachmentId
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Deleted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Success'
|
||||
|
||||
/api/search:
|
||||
get:
|
||||
summary: Full-text search
|
||||
description: Searches across note titles, content, and tags. Excludes trashed notes.
|
||||
tags: [Search]
|
||||
parameters:
|
||||
- name: q
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Matching notes
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Note'
|
||||
|
||||
/api/tags:
|
||||
get:
|
||||
summary: List all tags
|
||||
tags: [Tags]
|
||||
responses:
|
||||
'200':
|
||||
description: Array of tags
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Tag'
|
||||
|
||||
/api/sync:
|
||||
get:
|
||||
summary: Pull changes since timestamp
|
||||
tags: [Sync]
|
||||
parameters:
|
||||
- name: since
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: Unix timestamp in milliseconds
|
||||
responses:
|
||||
'200':
|
||||
description: Notes updated since the given timestamp
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Note'
|
||||
post:
|
||||
summary: Push local changes
|
||||
tags: [Sync]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [changes]
|
||||
properties:
|
||||
changes:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/SyncChange'
|
||||
responses:
|
||||
'200':
|
||||
description: Changes applied
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Success'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
sessionCookie:
|
||||
type: apiKey
|
||||
in: cookie
|
||||
name: session
|
||||
|
||||
schemas:
|
||||
Success:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
|
||||
Note:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
title:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
color:
|
||||
type: string
|
||||
enum: [default, coral, peach, sand, mint, sage, fog, storm, dusk, blossom, clay, chalk]
|
||||
pinned:
|
||||
type: boolean
|
||||
archived:
|
||||
type: boolean
|
||||
trashed:
|
||||
type: boolean
|
||||
checklistMode:
|
||||
type: boolean
|
||||
sortOrder:
|
||||
type: integer
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
version:
|
||||
type: integer
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
NoteInput:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
color:
|
||||
type: string
|
||||
enum: [default, coral, peach, sand, mint, sage, fog, storm, dusk, blossom, clay, chalk]
|
||||
pinned:
|
||||
type: boolean
|
||||
archived:
|
||||
type: boolean
|
||||
trashed:
|
||||
type: boolean
|
||||
checklistMode:
|
||||
type: boolean
|
||||
sortOrder:
|
||||
type: integer
|
||||
|
||||
Tag:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
|
||||
Attachment:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
noteId:
|
||||
type: string
|
||||
filename:
|
||||
type: string
|
||||
mimeType:
|
||||
type: string
|
||||
size:
|
||||
type: integer
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
SyncChange:
|
||||
type: object
|
||||
required: [noteId, operation, timestamp]
|
||||
properties:
|
||||
noteId:
|
||||
type: string
|
||||
format: uuid
|
||||
operation:
|
||||
type: string
|
||||
enum: [create, update, delete]
|
||||
timestamp:
|
||||
type: integer
|
||||
description: Unix timestamp in milliseconds
|
||||
data:
|
||||
$ref: '#/components/schemas/NoteInput'
|
||||
+17
-14
@@ -18,33 +18,36 @@
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"lint": "eslint ."
|
||||
"lint": "eslint .",
|
||||
"docs:api": "tsx scripts/generate-api-docs.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sveltejs/adapter-node": "^5.0.0",
|
||||
"argon2": "^0.43.0",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"drizzle-orm": "^0.44.0",
|
||||
"argon2": "^0.43.0",
|
||||
"idb": "^8.0.0",
|
||||
"uuid": "^11.0.0",
|
||||
"markdown-it": "^14.0.0"
|
||||
"markdown-it": "^14.0.0",
|
||||
"uuid": "^11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.0",
|
||||
"@sveltejs/kit": "^2.50.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"svelte": "^5.51.0",
|
||||
"svelte-check": "^4.4.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"@vite-pwa/sveltekit": "^1.0.0",
|
||||
"drizzle-kit": "^0.31.0",
|
||||
"vitest": "^3.0.0",
|
||||
"@playwright/test": "^1.50.0",
|
||||
"@types/better-sqlite3": "^7.6.0",
|
||||
"@types/markdown-it": "^14.0.0",
|
||||
"@types/uuid": "^10.0.0"
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vite-pwa/sveltekit": "^1.0.0",
|
||||
"drizzle-kit": "^0.31.0",
|
||||
"svelte": "^5.51.0",
|
||||
"svelte-check": "^4.4.2",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vitest": "^3.0.0",
|
||||
"yaml": "^2.8.2"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
|
||||
Generated
+64
-37
@@ -10,7 +10,7 @@ importers:
|
||||
dependencies:
|
||||
'@sveltejs/adapter-node':
|
||||
specifier: ^5.0.0
|
||||
version: 5.5.4(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))
|
||||
version: 5.5.4(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))
|
||||
argon2:
|
||||
specifier: ^0.43.0
|
||||
version: 0.43.1
|
||||
@@ -35,13 +35,13 @@ importers:
|
||||
version: 1.58.2
|
||||
'@sveltejs/kit':
|
||||
specifier: ^2.50.2
|
||||
version: 2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))
|
||||
version: 2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@sveltejs/vite-plugin-svelte':
|
||||
specifier: ^6.2.4
|
||||
version: 6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))
|
||||
version: 6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.0.0
|
||||
version: 4.2.1(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))
|
||||
version: 4.2.1(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@types/better-sqlite3':
|
||||
specifier: ^7.6.0
|
||||
version: 7.6.13
|
||||
@@ -53,7 +53,7 @@ importers:
|
||||
version: 10.0.0
|
||||
'@vite-pwa/sveltekit':
|
||||
specifier: ^1.0.0
|
||||
version: 1.1.0(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))(workbox-build@7.4.0)(workbox-window@7.4.0)
|
||||
version: 1.1.0(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(workbox-build@7.4.0)(workbox-window@7.4.0)
|
||||
drizzle-kit:
|
||||
specifier: ^0.31.0
|
||||
version: 0.31.9
|
||||
@@ -66,15 +66,21 @@ importers:
|
||||
tailwindcss:
|
||||
specifier: ^4.0.0
|
||||
version: 4.2.1
|
||||
tsx:
|
||||
specifier: ^4.21.0
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^7.3.1
|
||||
version: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)
|
||||
version: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vitest:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)
|
||||
version: 3.2.4(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
yaml:
|
||||
specifier: ^2.8.2
|
||||
version: 2.8.2
|
||||
|
||||
packages:
|
||||
|
||||
@@ -2798,6 +2804,11 @@ packages:
|
||||
tr46@1.0.1:
|
||||
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
|
||||
|
||||
tsx@4.21.0:
|
||||
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
tunnel-agent@0.6.0:
|
||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||
|
||||
@@ -3057,6 +3068,11 @@ packages:
|
||||
yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
|
||||
yaml@2.8.2:
|
||||
resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==}
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
zimmerframe@1.1.4:
|
||||
resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
|
||||
|
||||
@@ -4163,19 +4179,19 @@ snapshots:
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
|
||||
'@sveltejs/adapter-node@5.5.4(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))':
|
||||
'@sveltejs/adapter-node@5.5.4(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))':
|
||||
dependencies:
|
||||
'@rollup/plugin-commonjs': 29.0.1(rollup@4.59.0)
|
||||
'@rollup/plugin-json': 6.1.0(rollup@4.59.0)
|
||||
'@rollup/plugin-node-resolve': 16.0.3(rollup@4.59.0)
|
||||
'@sveltejs/kit': 2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))
|
||||
'@sveltejs/kit': 2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
rollup: 4.59.0
|
||||
|
||||
'@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))':
|
||||
'@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
'@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0)
|
||||
'@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))
|
||||
'@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@types/cookie': 0.6.0
|
||||
acorn: 8.16.0
|
||||
cookie: 0.6.0
|
||||
@@ -4187,26 +4203,26 @@ snapshots:
|
||||
set-cookie-parser: 3.0.1
|
||||
sirv: 3.0.2
|
||||
svelte: 5.53.7
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
'@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))':
|
||||
'@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))
|
||||
'@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
obug: 2.1.1
|
||||
svelte: 5.53.7
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
'@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))':
|
||||
'@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))
|
||||
'@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
deepmerge: 4.3.1
|
||||
magic-string: 0.30.21
|
||||
obug: 2.1.1
|
||||
svelte: 5.53.7
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)
|
||||
vitefu: 1.1.2(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vitefu: 1.1.2(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
|
||||
'@tailwindcss/node@4.2.1':
|
||||
dependencies:
|
||||
@@ -4269,12 +4285,12 @@ snapshots:
|
||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.1
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.2.1
|
||||
|
||||
'@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))':
|
||||
'@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@tailwindcss/node': 4.2.1
|
||||
'@tailwindcss/oxide': 4.2.1
|
||||
tailwindcss: 4.2.1
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
'@types/better-sqlite3@7.6.13':
|
||||
dependencies:
|
||||
@@ -4312,12 +4328,12 @@ snapshots:
|
||||
|
||||
'@types/uuid@10.0.0': {}
|
||||
|
||||
'@vite-pwa/sveltekit@1.1.0(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))(workbox-build@7.4.0)(workbox-window@7.4.0)':
|
||||
'@vite-pwa/sveltekit@1.1.0(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(workbox-build@7.4.0)(workbox-window@7.4.0)':
|
||||
dependencies:
|
||||
'@sveltejs/kit': 2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))
|
||||
'@sveltejs/kit': 2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
kolorist: 1.8.0
|
||||
tinyglobby: 0.2.15
|
||||
vite-plugin-pwa: 1.2.0(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))(workbox-build@7.4.0)(workbox-window@7.4.0)
|
||||
vite-plugin-pwa: 1.2.0(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(workbox-build@7.4.0)(workbox-window@7.4.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- vite
|
||||
@@ -4332,13 +4348,13 @@ snapshots:
|
||||
chai: 5.3.3
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))':
|
||||
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.4
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
dependencies:
|
||||
@@ -5725,6 +5741,13 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
tsx@4.21.0:
|
||||
dependencies:
|
||||
esbuild: 0.27.3
|
||||
get-tsconfig: 4.13.6
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
tunnel-agent@0.6.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
@@ -5806,13 +5829,13 @@ snapshots:
|
||||
|
||||
uuid@11.1.0: {}
|
||||
|
||||
vite-node@3.2.4(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0):
|
||||
vite-node@3.2.4(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.3
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 2.0.3
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
@@ -5827,18 +5850,18 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vite-plugin-pwa@1.2.0(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))(workbox-build@7.4.0)(workbox-window@7.4.0):
|
||||
vite-plugin-pwa@1.2.0(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(workbox-build@7.4.0)(workbox-window@7.4.0):
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
pretty-bytes: 6.1.1
|
||||
tinyglobby: 0.2.15
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
workbox-build: 7.4.0
|
||||
workbox-window: 7.4.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0):
|
||||
vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
esbuild: 0.27.3
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
@@ -5852,16 +5875,18 @@ snapshots:
|
||||
jiti: 2.6.1
|
||||
lightningcss: 1.31.1
|
||||
terser: 5.46.0
|
||||
tsx: 4.21.0
|
||||
yaml: 2.8.2
|
||||
|
||||
vitefu@1.1.2(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)):
|
||||
vitefu@1.1.2(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
vitest@3.2.4(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0):
|
||||
vitest@3.2.4(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/expect': 3.2.4
|
||||
'@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))
|
||||
'@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
'@vitest/runner': 3.2.4
|
||||
'@vitest/snapshot': 3.2.4
|
||||
@@ -5879,8 +5904,8 @@ snapshots:
|
||||
tinyglobby: 0.2.15
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)
|
||||
vite-node: 3.2.4(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite-node: 3.2.4(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 25.3.3
|
||||
@@ -6073,4 +6098,6 @@ snapshots:
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
yaml@2.8.2: {}
|
||||
|
||||
zimmerframe@1.1.4: {}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Generates docs/API.md from docs/openapi.yaml.
|
||||
*
|
||||
* Usage: npx tsx scripts/generate-api-docs.ts
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import { parse } from 'yaml';
|
||||
|
||||
const spec = parse(readFileSync('docs/openapi.yaml', 'utf-8'));
|
||||
|
||||
const lines: string[] = [
|
||||
'<!-- AUTO-GENERATED from docs/openapi.yaml — do not edit manually -->',
|
||||
'<!-- Run: pnpm docs:api -->',
|
||||
'',
|
||||
`# ${spec.info.title}`,
|
||||
'',
|
||||
spec.info.description,
|
||||
'',
|
||||
`Base URL: \`${spec.servers[0].url}\``,
|
||||
'',
|
||||
'All endpoints require a `session` cookie (set by login) except where noted.',
|
||||
''
|
||||
];
|
||||
|
||||
// Group paths by tag
|
||||
const groups = new Map<string, { method: string; path: string; op: Record<string, unknown> }[]>();
|
||||
|
||||
for (const [path, methods] of Object.entries(spec.paths) as [string, Record<string, unknown>][]) {
|
||||
const pathParams = (methods.parameters as unknown[]) || [];
|
||||
for (const [method, op] of Object.entries(methods)) {
|
||||
if (method === 'parameters') continue;
|
||||
const operation = op as Record<string, unknown>;
|
||||
const tag = ((operation.tags as string[]) || ['Other'])[0];
|
||||
if (!groups.has(tag)) groups.set(tag, []);
|
||||
groups.get(tag)!.push({ method: method.toUpperCase(), path, op: { ...operation, _pathParams: pathParams } });
|
||||
}
|
||||
}
|
||||
|
||||
for (const [tag, endpoints] of groups) {
|
||||
lines.push(`## ${tag}`, '');
|
||||
for (const { method, path, op } of endpoints) {
|
||||
lines.push(`### \`${method} ${path}\``, '');
|
||||
lines.push(String(op.summary || ''), '');
|
||||
if (op.description) lines.push(String(op.description), '');
|
||||
|
||||
if ((op.security as unknown[])?.length === 0) {
|
||||
lines.push('> No authentication required.', '');
|
||||
}
|
||||
|
||||
// Parameters
|
||||
const allParams = [...((op._pathParams as unknown[]) || []), ...((op.parameters as unknown[]) || [])];
|
||||
if (allParams.length > 0) {
|
||||
lines.push('**Parameters:**', '');
|
||||
lines.push('| Name | In | Type | Required | Description |');
|
||||
lines.push('|------|----|------|----------|-------------|');
|
||||
for (const p of allParams as Record<string, unknown>[]) {
|
||||
const schema = p.schema as Record<string, unknown> | undefined;
|
||||
const type = schema?.type || '';
|
||||
const enumVals = schema?.enum as string[] | undefined;
|
||||
let desc = String(p.description || schema?.description || '');
|
||||
if (enumVals) desc += ` (${enumVals.join(', ')})`;
|
||||
if (schema?.default !== undefined) desc += ` — default: \`${schema.default}\``;
|
||||
lines.push(`| \`${p.name}\` | ${p.in} | ${type} | ${p.required ? 'yes' : 'no'} | ${desc} |`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// Request body
|
||||
const body = op.requestBody as Record<string, unknown> | undefined;
|
||||
if (body) {
|
||||
const content = body.content as Record<string, Record<string, unknown>>;
|
||||
const contentType = Object.keys(content)[0];
|
||||
const schema = content[contentType]?.schema as Record<string, unknown> | undefined;
|
||||
|
||||
if (contentType === 'multipart/form-data') {
|
||||
lines.push('**Request:** `multipart/form-data` with `file` field.', '');
|
||||
} else if (schema) {
|
||||
const example = buildExample(schema, spec.components?.schemas as Record<string, unknown>);
|
||||
if (example) {
|
||||
lines.push('**Request:**', '', '```json', JSON.stringify(example, null, 2), '```', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responses
|
||||
const responses = op.responses as Record<string, Record<string, unknown>> | undefined;
|
||||
if (responses) {
|
||||
const codes = Object.keys(responses);
|
||||
const successCode = codes.find(c => c.startsWith('2')) || codes[0];
|
||||
const successResp = responses[successCode];
|
||||
lines.push(`**Response:** \`${successCode}\``, '');
|
||||
const respContent = successResp?.content as Record<string, Record<string, unknown>> | undefined;
|
||||
if (respContent) {
|
||||
const schema = Object.values(respContent)[0]?.schema as Record<string, unknown>;
|
||||
if (schema) {
|
||||
const example = buildExample(schema, spec.components?.schemas as Record<string, unknown>);
|
||||
if (example) {
|
||||
lines.push('```json', JSON.stringify(example, null, 2), '```', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error codes
|
||||
const errors = codes.filter(c => !c.startsWith('2'));
|
||||
if (errors.length > 0) {
|
||||
lines.push(`**Errors:** ${errors.map(c => `\`${c}\` ${responses[c].description || ''}`).join(', ')}`, '');
|
||||
}
|
||||
}
|
||||
|
||||
lines.push('---', '');
|
||||
}
|
||||
}
|
||||
|
||||
writeFileSync('docs/API.md', lines.join('\n'));
|
||||
console.log('Generated docs/API.md');
|
||||
|
||||
function buildExample(schema: Record<string, unknown>, schemas?: Record<string, unknown>): unknown {
|
||||
if (schema.$ref) {
|
||||
const refName = String(schema.$ref).split('/').pop()!;
|
||||
return schemas ? buildExample(schemas[refName] as Record<string, unknown>, schemas) : {};
|
||||
}
|
||||
|
||||
if (schema.type === 'array') {
|
||||
const items = schema.items as Record<string, unknown>;
|
||||
if (!items) return [];
|
||||
if (items.$ref || items.properties || items.type === 'object') return [buildExample(items, schemas)];
|
||||
return [exampleValue('item', items, schemas)];
|
||||
}
|
||||
|
||||
if (schema.type === 'object' || schema.properties) {
|
||||
const props = schema.properties as Record<string, Record<string, unknown>> | undefined;
|
||||
if (!props) return {};
|
||||
const obj: Record<string, unknown> = {};
|
||||
for (const [key, prop] of Object.entries(props)) {
|
||||
obj[key] = exampleValue(key, prop, schemas);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function exampleValue(key: string, prop: Record<string, unknown>, schemas?: Record<string, unknown>): unknown {
|
||||
if (prop.$ref) return buildExample(prop, schemas);
|
||||
if (prop.type === 'array') return buildExample(prop, schemas);
|
||||
if (prop.type === 'object' || prop.properties) return buildExample(prop, schemas);
|
||||
|
||||
const enumVals = prop.enum as string[] | undefined;
|
||||
if (enumVals) return enumVals[0];
|
||||
|
||||
switch (prop.type) {
|
||||
case 'string':
|
||||
if (prop.format === 'uuid') return 'uuid';
|
||||
if (prop.format === 'date-time') return '2025-01-01T00:00:00.000Z';
|
||||
if (prop.format === 'binary') return '(binary)';
|
||||
return key;
|
||||
case 'integer':
|
||||
case 'number':
|
||||
return prop.default !== undefined ? prop.default : 1;
|
||||
case 'boolean':
|
||||
return prop.default !== undefined ? prop.default : true;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user