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-generate` | `pnpm db:generate` | Generate migrations |
|
||||||
| `make db-migrate` | `pnpm db:migrate` | Run migrations |
|
| `make db-migrate` | `pnpm db:migrate` | Run migrations |
|
||||||
| `make db-studio` | `pnpm db:studio` | Open Drizzle Studio |
|
| `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 install` | `pnpm install` | Install dependencies |
|
||||||
| `make docker-build` | — | Build Docker image |
|
| `make docker-build` | — | Build Docker image |
|
||||||
| `make docker-up` | — | Start containers |
|
| `make docker-up` | — | Start containers |
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.PHONY: help dev build preview check test test-unit test-e2e lint \
|
.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
|
install docker-build docker-up docker-down docker-logs clean
|
||||||
|
|
||||||
help: ## List all targets
|
help: ## List all targets
|
||||||
@@ -41,6 +41,9 @@ db-migrate: ## Run migrations
|
|||||||
db-studio: ## Open Drizzle Studio
|
db-studio: ## Open Drizzle Studio
|
||||||
pnpm db:studio
|
pnpm db:studio
|
||||||
|
|
||||||
|
docs-api: ## Regenerate API docs from OpenAPI spec
|
||||||
|
pnpm docs:api
|
||||||
|
|
||||||
install: ## Install dependencies
|
install: ## Install dependencies
|
||||||
pnpm install
|
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.
|
A self-hostable, offline-first note-taking app inspired by Google Keep. Part of the [Bretzel](https://bretzel.app) app universe.
|
||||||
|
|
||||||
## 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`
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -33,118 +15,60 @@ Open http://localhost:3000 and set your password on first visit.
|
|||||||
### Development
|
### Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
|
||||||
pnpm install
|
pnpm install
|
||||||
|
|
||||||
# Start dev server
|
|
||||||
pnpm dev
|
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
|
## Tech Stack
|
||||||
|
|
||||||
| Layer | Technology |
|
| Layer | Technology |
|
||||||
|-------|-----------|
|
|-------|-----------|
|
||||||
| Framework | SvelteKit 2 |
|
| Framework | SvelteKit 2 (Svelte 5 runes) |
|
||||||
| Language | TypeScript (strict) |
|
| Language | TypeScript (strict) |
|
||||||
| UI | Tailwind CSS 4 |
|
| UI | Tailwind CSS 4 |
|
||||||
| Database | SQLite (better-sqlite3) |
|
| Database | SQLite (better-sqlite3) + Drizzle ORM |
|
||||||
| ORM | Drizzle ORM |
|
|
||||||
| Client DB | IndexedDB (idb) |
|
| Client DB | IndexedDB (idb) |
|
||||||
| Sync | LWW CRDTs |
|
| Sync | LWW CRDTs |
|
||||||
| Auth | Argon2 + session cookies |
|
| Auth | Argon2 + session cookies |
|
||||||
| Markdown | markdown-it |
|
|
||||||
| PWA | @vite-pwa/sveltekit |
|
|
||||||
| Testing | Vitest + Playwright |
|
| Testing | Vitest + Playwright |
|
||||||
| Container | Docker (multi-stage) |
|
| Container | Docker (multi-stage) |
|
||||||
| CI/CD | GitHub Actions |
|
| CI/CD | GitHub Actions |
|
||||||
|
|
||||||
## Project Structure
|
## Documentation
|
||||||
|
|
||||||
```
|
| Document | Description |
|
||||||
src/
|
|----------|-------------|
|
||||||
├── lib/
|
| [Features](docs/FEATURES.md) | Detailed feature list and behavior |
|
||||||
│ ├── components/ # Svelte UI components
|
| [Architecture](docs/ARCHITECTURE.md) | Local-first sync design, tech rationale, DB schema |
|
||||||
│ ├── server/db/ # Drizzle schema & connection
|
| [Deployment](docs/DEPLOYMENT.md) | Docker, Node.js, reverse proxy, backups, env vars |
|
||||||
│ ├── server/auth.ts # Authentication logic
|
| [API](docs/API.md) | REST API reference (auto-generated) |
|
||||||
│ ├── 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 |
|
|
||||||
|
|
||||||
## CI/CD
|
## 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
|
Configure registry via GitHub Secrets: `REGISTRY_URL`, `REGISTRY_USER`, `REGISTRY_TOKEN`.
|
||||||
- **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.
|
|
||||||
|
|
||||||
## License
|
## 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
|
Base URL: `http://localhost:3000`
|
||||||
First-time setup. Creates the single user account.
|
|
||||||
|
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:**
|
**Request:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "password": "min8characters" }
|
{
|
||||||
|
"password": "password"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:** `201 Created`
|
**Response:** `201`
|
||||||
|
|
||||||
```json
|
```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:**
|
**Request:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "password": "yourpassword" }
|
{
|
||||||
|
"password": "password"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:** `200 OK`
|
**Response:** `200`
|
||||||
```json
|
|
||||||
{ "success": true }
|
|
||||||
```
|
|
||||||
Sets `session` cookie.
|
|
||||||
|
|
||||||
### POST /api/auth/logout
|
|
||||||
Clears session.
|
|
||||||
|
|
||||||
**Response:** `200 OK`
|
|
||||||
```json
|
```json
|
||||||
{ "success": true }
|
{
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Errors:** `401` Invalid password
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `POST /api/auth/logout`
|
||||||
|
|
||||||
|
Log out
|
||||||
|
|
||||||
|
**Response:** `200`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
### GET /api/notes
|
### `GET /api/notes`
|
||||||
List notes by filter.
|
|
||||||
|
|
||||||
**Query Parameters:**
|
List notes
|
||||||
- `filter` - `all` (default), `archived`, `trashed`
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Name | In | Type | Required | Description |
|
||||||
|
|------|----|------|----------|-------------|
|
||||||
|
| `filter` | query | string | no | (all, archived, trashed) — default: `all` |
|
||||||
|
|
||||||
|
**Response:** `200`
|
||||||
|
|
||||||
**Response:** `200 OK`
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "uuid",
|
"id": "uuid",
|
||||||
"title": "My Note",
|
"title": "title",
|
||||||
"content": "**Markdown** content",
|
"content": "content",
|
||||||
"color": "coral",
|
"color": "default",
|
||||||
"pinned": true,
|
"pinned": true,
|
||||||
"archived": false,
|
"archived": true,
|
||||||
"trashed": false,
|
"trashed": true,
|
||||||
"checklistMode": false,
|
"checklistMode": true,
|
||||||
"sortOrder": 0,
|
"sortOrder": 1,
|
||||||
"createdAt": "2024-01-01T00:00:00.000Z",
|
"createdAt": "2025-01-01T00:00:00.000Z",
|
||||||
"updatedAt": "2024-01-01T00:00:00.000Z",
|
"updatedAt": "2025-01-01T00:00:00.000Z",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"tags": ["work", "important"]
|
"tags": [
|
||||||
|
"item"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
### POST /api/notes
|
---
|
||||||
Create a new note.
|
|
||||||
|
### `POST /api/notes`
|
||||||
|
|
||||||
|
Create a note
|
||||||
|
|
||||||
**Request:**
|
**Request:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"title": "My Note",
|
"title": "title",
|
||||||
"content": "Note content with #tags",
|
"content": "content",
|
||||||
"color": "default",
|
"color": "default",
|
||||||
"pinned": false,
|
"pinned": true,
|
||||||
"checklistMode": false
|
"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
|
```json
|
||||||
{
|
{
|
||||||
"title": "Updated Title",
|
"id": "uuid",
|
||||||
"color": "coral",
|
"title": "title",
|
||||||
"pinned": true
|
"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
|
### `GET /api/notes/{id}`
|
||||||
Permanently delete a note.
|
|
||||||
|
|
||||||
## Search
|
Get a note
|
||||||
|
|
||||||
### GET /api/search
|
**Parameters:**
|
||||||
Full-text search across notes.
|
|
||||||
|
|
||||||
**Query Parameters:**
|
| Name | In | Type | Required | Description |
|
||||||
- `q` - Search query
|
|------|----|------|----------|-------------|
|
||||||
|
| `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
|
```json
|
||||||
[
|
{
|
||||||
{ "id": 1, "name": "work" },
|
"id": "uuid",
|
||||||
{ "id": 2, "name": "personal" }
|
"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
|
## Attachments
|
||||||
|
|
||||||
### GET /api/notes/:id/attachments
|
### `GET /api/notes/{id}/attachments`
|
||||||
List attachments for a note.
|
|
||||||
|
|
||||||
### POST /api/notes/:id/attachments
|
List or download attachments
|
||||||
Upload an image attachment.
|
|
||||||
|
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.
|
**Request:** `multipart/form-data` with `file` field.
|
||||||
|
|
||||||
**Response:** `201 Created` - Returns attachment metadata.
|
**Response:** `201`
|
||||||
|
|
||||||
### DELETE /api/notes/:id/attachments?attachmentId=xxx
|
```json
|
||||||
Delete an attachment.
|
{
|
||||||
|
"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
|
## Sync
|
||||||
|
|
||||||
### POST /api/sync
|
### `GET /api/sync`
|
||||||
Push local changes to server.
|
|
||||||
|
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:**
|
**Request:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"changes": [
|
"changes": [
|
||||||
{
|
{
|
||||||
"noteId": "uuid",
|
"noteId": "uuid",
|
||||||
"operation": "create|update|delete",
|
"operation": "create",
|
||||||
"timestamp": 1704067200000,
|
"timestamp": 1,
|
||||||
"data": { "title": "...", "content": "..." }
|
"data": {
|
||||||
|
"title": "title",
|
||||||
|
"content": "content",
|
||||||
|
"color": "default",
|
||||||
|
"pinned": true,
|
||||||
|
"archived": true,
|
||||||
|
"trashed": true,
|
||||||
|
"checklistMode": true,
|
||||||
|
"sortOrder": 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### GET /api/sync?since=timestamp
|
**Response:** `200`
|
||||||
Pull changes since a timestamp (milliseconds).
|
|
||||||
|
|
||||||
**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:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
"lint": "eslint ."
|
"lint": "eslint .",
|
||||||
|
"docs:api": "tsx scripts/generate-api-docs.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/adapter-node": "^5.0.0",
|
"@sveltejs/adapter-node": "^5.0.0",
|
||||||
|
"argon2": "^0.43.0",
|
||||||
"better-sqlite3": "^11.0.0",
|
"better-sqlite3": "^11.0.0",
|
||||||
"drizzle-orm": "^0.44.0",
|
"drizzle-orm": "^0.44.0",
|
||||||
"argon2": "^0.43.0",
|
|
||||||
"idb": "^8.0.0",
|
"idb": "^8.0.0",
|
||||||
"uuid": "^11.0.0",
|
"markdown-it": "^14.0.0",
|
||||||
"markdown-it": "^14.0.0"
|
"uuid": "^11.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.50.0",
|
||||||
"@sveltejs/kit": "^2.50.2",
|
"@sveltejs/kit": "^2.50.2",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
"@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/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/better-sqlite3": "^7.6.0",
|
||||||
"@types/markdown-it": "^14.0.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": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
|||||||
Generated
+64
-37
@@ -10,7 +10,7 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@sveltejs/adapter-node':
|
'@sveltejs/adapter-node':
|
||||||
specifier: ^5.0.0
|
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:
|
argon2:
|
||||||
specifier: ^0.43.0
|
specifier: ^0.43.0
|
||||||
version: 0.43.1
|
version: 0.43.1
|
||||||
@@ -35,13 +35,13 @@ importers:
|
|||||||
version: 1.58.2
|
version: 1.58.2
|
||||||
'@sveltejs/kit':
|
'@sveltejs/kit':
|
||||||
specifier: ^2.50.2
|
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':
|
'@sveltejs/vite-plugin-svelte':
|
||||||
specifier: ^6.2.4
|
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':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.0.0
|
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':
|
'@types/better-sqlite3':
|
||||||
specifier: ^7.6.0
|
specifier: ^7.6.0
|
||||||
version: 7.6.13
|
version: 7.6.13
|
||||||
@@ -53,7 +53,7 @@ importers:
|
|||||||
version: 10.0.0
|
version: 10.0.0
|
||||||
'@vite-pwa/sveltekit':
|
'@vite-pwa/sveltekit':
|
||||||
specifier: ^1.0.0
|
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:
|
drizzle-kit:
|
||||||
specifier: ^0.31.0
|
specifier: ^0.31.0
|
||||||
version: 0.31.9
|
version: 0.31.9
|
||||||
@@ -66,15 +66,21 @@ importers:
|
|||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.2.1
|
version: 4.2.1
|
||||||
|
tsx:
|
||||||
|
specifier: ^4.21.0
|
||||||
|
version: 4.21.0
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.9.3
|
specifier: ^5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vite:
|
vite:
|
||||||
specifier: ^7.3.1
|
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:
|
vitest:
|
||||||
specifier: ^3.0.0
|
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:
|
packages:
|
||||||
|
|
||||||
@@ -2798,6 +2804,11 @@ packages:
|
|||||||
tr46@1.0.1:
|
tr46@1.0.1:
|
||||||
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
|
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:
|
tunnel-agent@0.6.0:
|
||||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||||
|
|
||||||
@@ -3057,6 +3068,11 @@ packages:
|
|||||||
yallist@3.1.1:
|
yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
|
|
||||||
|
yaml@2.8.2:
|
||||||
|
resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==}
|
||||||
|
engines: {node: '>= 14.6'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
zimmerframe@1.1.4:
|
zimmerframe@1.1.4:
|
||||||
resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
|
resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
|
||||||
|
|
||||||
@@ -4163,19 +4179,19 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.16.0
|
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:
|
dependencies:
|
||||||
'@rollup/plugin-commonjs': 29.0.1(rollup@4.59.0)
|
'@rollup/plugin-commonjs': 29.0.1(rollup@4.59.0)
|
||||||
'@rollup/plugin-json': 6.1.0(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)
|
'@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
|
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:
|
dependencies:
|
||||||
'@standard-schema/spec': 1.1.0
|
'@standard-schema/spec': 1.1.0
|
||||||
'@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.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
|
'@types/cookie': 0.6.0
|
||||||
acorn: 8.16.0
|
acorn: 8.16.0
|
||||||
cookie: 0.6.0
|
cookie: 0.6.0
|
||||||
@@ -4187,26 +4203,26 @@ snapshots:
|
|||||||
set-cookie-parser: 3.0.1
|
set-cookie-parser: 3.0.1
|
||||||
sirv: 3.0.2
|
sirv: 3.0.2
|
||||||
svelte: 5.53.7
|
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:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
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:
|
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
|
obug: 2.1.1
|
||||||
svelte: 5.53.7
|
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:
|
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
|
deepmerge: 4.3.1
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
obug: 2.1.1
|
obug: 2.1.1
|
||||||
svelte: 5.53.7
|
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)
|
||||||
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))
|
||||||
|
|
||||||
'@tailwindcss/node@4.2.1':
|
'@tailwindcss/node@4.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4269,12 +4285,12 @@ snapshots:
|
|||||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.1
|
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.1
|
||||||
'@tailwindcss/oxide-win32-x64-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:
|
dependencies:
|
||||||
'@tailwindcss/node': 4.2.1
|
'@tailwindcss/node': 4.2.1
|
||||||
'@tailwindcss/oxide': 4.2.1
|
'@tailwindcss/oxide': 4.2.1
|
||||||
tailwindcss: 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':
|
'@types/better-sqlite3@7.6.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4312,12 +4328,12 @@ snapshots:
|
|||||||
|
|
||||||
'@types/uuid@10.0.0': {}
|
'@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:
|
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
|
kolorist: 1.8.0
|
||||||
tinyglobby: 0.2.15
|
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:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
- vite
|
- vite
|
||||||
@@ -4332,13 +4348,13 @@ snapshots:
|
|||||||
chai: 5.3.3
|
chai: 5.3.3
|
||||||
tinyrainbow: 2.0.0
|
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:
|
dependencies:
|
||||||
'@vitest/spy': 3.2.4
|
'@vitest/spy': 3.2.4
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
optionalDependencies:
|
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':
|
'@vitest/pretty-format@3.2.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5725,6 +5741,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
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:
|
tunnel-agent@0.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
@@ -5806,13 +5829,13 @@ snapshots:
|
|||||||
|
|
||||||
uuid@11.1.0: {}
|
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:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
es-module-lexer: 1.7.0
|
es-module-lexer: 1.7.0
|
||||||
pathe: 2.0.3
|
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:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- jiti
|
- jiti
|
||||||
@@ -5827,18 +5850,18 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- 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:
|
dependencies:
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
pretty-bytes: 6.1.1
|
pretty-bytes: 6.1.1
|
||||||
tinyglobby: 0.2.15
|
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-build: 7.4.0
|
||||||
workbox-window: 7.4.0
|
workbox-window: 7.4.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
esbuild: 0.27.3
|
esbuild: 0.27.3
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
@@ -5852,16 +5875,18 @@ snapshots:
|
|||||||
jiti: 2.6.1
|
jiti: 2.6.1
|
||||||
lightningcss: 1.31.1
|
lightningcss: 1.31.1
|
||||||
terser: 5.46.0
|
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:
|
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:
|
dependencies:
|
||||||
'@types/chai': 5.2.3
|
'@types/chai': 5.2.3
|
||||||
'@vitest/expect': 3.2.4
|
'@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/pretty-format': 3.2.4
|
||||||
'@vitest/runner': 3.2.4
|
'@vitest/runner': 3.2.4
|
||||||
'@vitest/snapshot': 3.2.4
|
'@vitest/snapshot': 3.2.4
|
||||||
@@ -5879,8 +5904,8 @@ snapshots:
|
|||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
tinypool: 1.1.1
|
tinypool: 1.1.1
|
||||||
tinyrainbow: 2.0.0
|
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: 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)
|
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
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 25.3.3
|
'@types/node': 25.3.3
|
||||||
@@ -6073,4 +6098,6 @@ snapshots:
|
|||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
||||||
|
yaml@2.8.2: {}
|
||||||
|
|
||||||
zimmerframe@1.1.4: {}
|
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