mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
1225 lines
44 KiB
Markdown
1225 lines
44 KiB
Markdown
# Contributing to Profilarr
|
|
|
|
## About
|
|
|
|
Profilarr is a configuration management tool for Radarr and Sonarr.
|
|
|
|
Setting up media automation properly means creating dozens of custom formats to
|
|
identify things like 4K releases, preferred encoders, and quality thresholds.
|
|
Then you need to orchestrate all those formats into quality profiles that
|
|
actually work together. Most people spend hours piecing this together from forum
|
|
posts and guides.
|
|
|
|
Profilarr lets users pull from shared configuration databases instead of
|
|
building everything from scratch. You link a database, connect your arr
|
|
instances, and sync. It compiles the configurations, pushes them to your apps,
|
|
preserves any local modifications you've made, and tracks everything with git so
|
|
you can see what changed.
|
|
|
|
### Users and Developers
|
|
|
|
Profilarr serves two audiences. End users link external databases and sync
|
|
configurations to their Arr instances. Developers create and maintain those
|
|
databases.
|
|
|
|
The editing interface serves both. End users can make custom tweaks to profiles
|
|
or formats after syncing—these local modifications persist across future syncs.
|
|
Developers use the same editors to build databases from scratch, test their
|
|
configurations, and iterate on profiles before publishing.
|
|
|
|
## Stack
|
|
|
|
SvelteKit with Svelte 5 running on Deno. We don't use runes—event handlers use
|
|
`onclick` syntax, but no `$state`, `$derived`, or other rune primitives.
|
|
Tailwind CSS 4 for styling. Lucide and Simple Icons for iconography.
|
|
|
|
Both `deno.json` and `package.json` exist in the project:
|
|
|
|
- **deno.json** defines import maps (path aliases like `$lib/`, `$stores/`),
|
|
Deno-specific imports from JSR (`jsr:@std/yaml`), and all runnable tasks
|
|
(`deno task dev`, `deno task build`, etc.)
|
|
|
|
- **package.json** provides npm dependencies that Vite and SvelteKit need during
|
|
the build process. The `@deno/vite-plugin` and `sveltekit-adapter-deno`
|
|
packages bridge these two ecosystems.
|
|
|
|
When you run `deno task dev`, Deno resolves imports through `deno.json` while
|
|
Vite pulls dependencies from `node_modules`. Both files are required—`deno.json`
|
|
for runtime resolution and tasks, `package.json` for the Vite build toolchain.
|
|
|
|
### Frontend
|
|
|
|
The UI lets users:
|
|
|
|
- **Link databases** — Connect external GitHub repositories containing
|
|
Profilarr-compliant configurations. Supports public and private repos with
|
|
auth tokens.
|
|
|
|
- **Connect Arr instances** — Add Radarr, Sonarr, Lidarr, or Chaptarr instances
|
|
by URL and API key.
|
|
|
|
- **Manage entities** — Create and edit quality profiles, custom formats, delay
|
|
profiles, media management settings, and regular expressions. Each entity type
|
|
has its own editor with testing capabilities.
|
|
|
|
- **Configure sync** — Set up how and when configurations push to each Arr
|
|
instance. Strategies include manual, scheduled (cron), on-pull, and on-change.
|
|
Dependencies like custom formats auto-sync with their parent profiles.
|
|
|
|
- **Browse libraries** — View downloaded media with filtering, sorting, and bulk
|
|
profile reassignment (Radarr only currently).
|
|
|
|
- **Manage upgrades** — Configure automatic quality upgrades with filters,
|
|
schedules, and dry-run testing.
|
|
|
|
- **Settings** — Notifications (Discord/Slack/Email), backups, logging, theming,
|
|
and background job management.
|
|
|
|
- **Test quality profiles** — Validate how custom formats score against real
|
|
release titles. Add test entities (movies/series from TMDB), attach release
|
|
titles, and see which formats match and how they score.
|
|
|
|
#### Entity Testing
|
|
|
|
Entity testing lets users validate quality profiles before syncing to Arr
|
|
instances. The workflow:
|
|
|
|
1. Add test entities (movies or TV series) via TMDB search
|
|
2. Attach release titles to each entity—either manually or by importing from a
|
|
connected Arr instance
|
|
3. Select a quality profile to see how each release would score
|
|
4. Expand releases to see parsed metadata and which custom formats matched
|
|
|
|
**Architecture:**
|
|
|
|
- **Lazy evaluation** — Release parsing and CF evaluation happen on-demand when
|
|
an entity row is expanded, not on page load. This keeps the page fast even
|
|
with many entities/releases.
|
|
- **Parser service** — Release titles are parsed by the C# parser microservice
|
|
to extract metadata (resolution, source, languages, release group, etc.).
|
|
Pattern matching uses .NET-compatible regex for accuracy with Arr behavior.
|
|
- **Caching** — Both parsed results and pattern match results are cached in
|
|
SQLite. Cache keys include parser version (for parse cache) or pattern hash
|
|
(for match cache) to auto-invalidate when things change.
|
|
|
|
**API Endpoints:**
|
|
|
|
- `POST /api/v1/entity-testing/evaluate` — Parses releases and evaluates them
|
|
against all custom formats. Returns which CFs matched each release.
|
|
- `GET /api/v1/arr/library` — Fetches movie/series library from an Arr instance
|
|
for release import.
|
|
- `GET /api/v1/arr/releases` — Triggers interactive search on an Arr instance
|
|
and returns grouped/deduplicated releases.
|
|
|
|
See `docs/todo/1.automatic-entity-release-add.md` for detailed API research and
|
|
the release import implementation plan.
|
|
|
|
**Key Files:**
|
|
|
|
- `src/routes/quality-profiles/entity-testing/` — Page and components
|
|
- `src/routes/api/v1/entity-testing/evaluate/+server.ts` — Evaluation endpoint
|
|
- `src/routes/api/v1/arr/library/+server.ts` — Library fetch endpoint
|
|
- `src/routes/api/v1/arr/releases/+server.ts` — Release search endpoint
|
|
- `src/lib/server/pcd/queries/customFormats/evaluator.ts` — CF evaluation logic
|
|
- `src/lib/server/utils/arr/parser/client.ts` — Parser client with caching
|
|
|
|
#### Routes, Not Modals
|
|
|
|
Prefer routes over modals. Modals should only be used for things requiring
|
|
immediate attention—confirmations like "you have unsaved changes" or "are you
|
|
sure you want to delete this?" They can also display supplementary information
|
|
about a page that wouldn't fit in the layout otherwise.
|
|
|
|
In rare cases, modals can be used for one-time forms. Use this sparingly and
|
|
only when a route would be excessively nested. The only place we do this is for
|
|
adding test entities and releases to those entities. Without modals there, we'd
|
|
be 5-6 routes deep and the breadcrumbs become confusing.
|
|
|
|
Examples:
|
|
|
|
- `src/routes/databases/+page.svelte` — Confirmation modal for unlinking a
|
|
database. Warns about data loss before proceeding.
|
|
- `src/routes/arr/+page.svelte` — Confirmation modal for deleting an Arr
|
|
instance.
|
|
- `src/routes/settings/backups/+page.svelte` — Confirmation modals for both
|
|
deleting and restoring backups.
|
|
- `src/routes/arr/[id]/upgrades/components/UpgradesInfoModal.svelte` — Info
|
|
modal explaining how the upgrades module works. Too much content for the page
|
|
itself.
|
|
- `src/routes/quality-profiles/entity-testing/[databaseId]/components/AddEntityModal.svelte`
|
|
— One-time form exception. Searches TMDB and adds test entities. A route here
|
|
would be 5+ levels deep.
|
|
- `src/routes/quality-profiles/entity-testing/[databaseId]/components/ReleaseModal.svelte`
|
|
— One-time form exception. Adds test releases to entities.
|
|
|
|
#### Alerts
|
|
|
|
Users need feedback when they take an action. Use the alert system in
|
|
`src/lib/client/alerts/` to show success, error, warning, or info messages.
|
|
Import `alertStore` and call `alertStore.add(type, message)`.
|
|
|
|
Examples:
|
|
|
|
- `src/routes/arr/components/InstanceForm.svelte` — Shows success/error after
|
|
testing connection, creating, or updating an Arr instance.
|
|
- `src/routes/databases/components/InstanceForm.svelte` — Shows success/error
|
|
when linking, updating, or unlinking databases.
|
|
- `src/routes/settings/general/components/TMDBSettings.svelte` — Shows
|
|
success/error after testing TMDB API connection.
|
|
- `src/routes/settings/jobs/components/JobCard.svelte` — Shows success/error
|
|
when triggering or toggling background jobs.
|
|
- `src/routes/quality-profiles/entity-testing/[databaseId]/+page.svelte` — Shows
|
|
a persistent warning (duration: 0) when the parser service is unavailable.
|
|
|
|
#### Dirty Tracking
|
|
|
|
The dirty store (`src/lib/client/stores/dirty.ts`) tracks whether a form has
|
|
unsaved changes by comparing current state against an original snapshot. This
|
|
serves two purposes: disabling save buttons when nothing has changed (avoiding
|
|
unnecessary requests and file writes), and warning users before they navigate
|
|
away from unsaved work via `DirtyModal.svelte`.
|
|
|
|
How it works:
|
|
|
|
1. **Initialize** — Call `initEdit(serverData)` for existing records or
|
|
`initCreate(defaults)` for new ones. This captures the original snapshot.
|
|
2. **Update** — Call `update(field, value)` when a field changes. The store
|
|
compares current state against the snapshot using deep equality.
|
|
3. **Check** — Subscribe to `$isDirty` to enable/disable save buttons or show
|
|
warnings.
|
|
4. **Reset** — Call `initEdit(newServerData)` after a successful save to capture
|
|
the new baseline.
|
|
|
|
Examples:
|
|
|
|
- `src/routes/quality-profiles/[databaseId]/[id]/languages/+page.svelte` —
|
|
Tracks language selection changes. Save button disabled until dirty.
|
|
- `src/routes/quality-profiles/[databaseId]/[id]/qualities/+page.svelte` —
|
|
Tracks drag-and-drop reordering of quality tiers. Shows a sticky save bar only
|
|
when dirty.
|
|
- `src/routes/custom-formats/[databaseId]/components/GeneralForm.svelte` —
|
|
Handles both create and edit modes. Uses `initCreate()` for new formats,
|
|
`initEdit()` for existing ones.
|
|
- `src/routes/arr/[id]/sync/+page.svelte` — Aggregates dirty state from three
|
|
child components (QualityProfiles, DelayProfiles, MediaManagement). Prevents
|
|
syncing while there are unsaved changes.
|
|
- `src/lib/client/ui/modal/DirtyModal.svelte` — Global navigation guard. Uses
|
|
`beforeNavigate` to intercept route changes and prompt the user if dirty.
|
|
|
|
#### Actions Bar
|
|
|
|
Entity list pages use a horizontal toolbar for filters, search, and view
|
|
toggles. The components live in `src/lib/client/ui/actions/`:
|
|
|
|
- **ActionsBar** — Container that groups child components. Uses negative margins
|
|
and CSS to make buttons appear connected (shared borders, rounded corners only
|
|
on the ends).
|
|
- **ActionButton** — Icon button with optional hover dropdown. Can be square or
|
|
variable width.
|
|
- **SearchAction** — Search input with debounce, integrates with a search store.
|
|
- **ViewToggle** — Dropdown to switch between card and table views.
|
|
|
|
**Do not add custom margins, gaps, or wrapper divs between items inside
|
|
ActionsBar.** The component relies on direct children to calculate border
|
|
radius. Adding spacing breaks the connected appearance.
|
|
|
|
```svelte
|
|
<!-- Correct -->
|
|
<ActionsBar>
|
|
<ActionButton icon={Filter} />
|
|
<SearchAction {searchStore} />
|
|
<ViewToggle bind:value={viewMode} />
|
|
</ActionsBar>
|
|
|
|
<!-- Wrong - gap breaks connected styling -->
|
|
<ActionsBar class="gap-2">
|
|
<ActionButton icon={Filter} />
|
|
<SearchAction {searchStore} />
|
|
</ActionsBar>
|
|
|
|
<!-- Wrong - wrapper div breaks border radius -->
|
|
<ActionsBar>
|
|
<div class="flex">
|
|
<ActionButton icon={Filter} />
|
|
</div>
|
|
<SearchAction {searchStore} />
|
|
</ActionsBar>
|
|
```
|
|
|
|
Examples:
|
|
|
|
- `src/routes/quality-profiles/[databaseId]/+page.svelte`
|
|
- `src/routes/custom-formats/[databaseId]/+page.svelte`
|
|
- `src/routes/arr/[id]/library/components/LibraryActionBar.svelte`
|
|
|
|
#### Dropdowns
|
|
|
|
Hover-triggered dropdown menus live in `src/lib/client/ui/dropdown/`:
|
|
|
|
- **Dropdown** — Positioned container for dropdown content. Has an invisible
|
|
hover bridge so the menu stays open when moving the mouse from trigger to
|
|
content. Supports `left`, `right`, or `middle` positioning.
|
|
- **DropdownItem** — Individual menu item with icon, label, and optional
|
|
`selected` checkmark. Supports `disabled` and `danger` variants.
|
|
- **CustomGroupManager** — Specialized component for managing user-defined
|
|
filter groups (used in library filtering).
|
|
|
|
Dropdowns are typically placed inside an `ActionButton` with
|
|
`hasDropdown={true}` using the `slot="dropdown"` pattern. See
|
|
`ViewToggle.svelte` for a simple example.
|
|
|
|
#### Tables
|
|
|
|
Data tables live in `src/lib/client/ui/table/`:
|
|
|
|
- **Table** — Generic data table with typed column definitions. Supports sorting
|
|
(click column headers to cycle asc/desc/none), custom cell renderers, row
|
|
click handlers, compact mode, and an `actions` slot for row-level buttons.
|
|
Column definitions include `key`, `header`, `sortable`, `align`, `width`, and
|
|
optional `cell` render function.
|
|
|
|
- **ExpandableTable** — Rows expand on click to reveal additional content via
|
|
the `expanded` slot. Chevron indicators show expand state. Supports
|
|
`chevronPosition` (left/right), `flushExpanded` for edge-to-edge content, and
|
|
external control of `expandedRows`.
|
|
|
|
- **ReorderableList** — Drag-and-drop list for reordering items. Uses a
|
|
sensitivity threshold to prevent flickering during drags. Calls `onReorder`
|
|
with the new array after each move.
|
|
|
|
Column types are defined in `types.ts`. Key properties:
|
|
|
|
- `sortable` — enables click-to-sort on the column header
|
|
- `sortAccessor` — function to extract the sort value (useful when display
|
|
differs from sort order)
|
|
- `sortComparator` — custom comparison function for complex sorting
|
|
- `cell` — render function returning a string, HTML object, or Svelte component
|
|
|
|
#### Stores
|
|
|
|
Svelte stores live in `src/lib/client/stores/`. Two patterns exist: factory
|
|
functions that create store instances, and singleton stores.
|
|
|
|
**Factory Stores**
|
|
|
|
Use factory functions when each page needs its own isolated state:
|
|
|
|
- `createSearchStore()` — Debounced search with filters. Returns methods for
|
|
`setQuery()`, `setFilter()`, `clear()`, and a `filterItems()` helper.
|
|
- `createDataPageStore()` — Combines search with view toggle (table/cards).
|
|
Persists view mode to localStorage. Returns `search`, `view`, and `filtered`
|
|
derived store.
|
|
|
|
```typescript
|
|
import { createDataPageStore } from "$stores/dataPage";
|
|
|
|
const { search, view, filtered } = createDataPageStore(data.profiles, {
|
|
storageKey: "qualityProfilesView",
|
|
searchKeys: ["name", "description"],
|
|
});
|
|
|
|
// Use in template
|
|
{#each $filtered as profile}
|
|
```
|
|
|
|
**Singleton Stores**
|
|
|
|
Export instances directly for app-wide state:
|
|
|
|
- `themeStore` — Dark/light mode
|
|
- `accentStore` — Accent color
|
|
- `sidebarCollapsed` — Sidebar state
|
|
- `alertStore` — Global alerts (imported via `$alerts/store`)
|
|
- `libraryCache` — Per-instance library data cache
|
|
|
|
**Dirty Store**
|
|
|
|
The dirty store (`dirty.ts`) is documented above in Dirty Tracking. It's a
|
|
singleton but with methods that make it behave like a state machine for form
|
|
change detection.
|
|
|
|
### Backend
|
|
|
|
Server-side code lives in `src/lib/server/`. Profilarr uses two separate data
|
|
stores: the main SQLite database for application state (Arr connections,
|
|
settings, job history), and PCD git repositories for versioned configuration
|
|
(profiles, formats, media settings). The main database tracks _which_ PCDs are
|
|
linked and _how_ to sync them. The PCDs contain _what_ gets synced.
|
|
|
|
Key directories:
|
|
|
|
- **db/** — Main SQLite database (app state, settings, job history)
|
|
- **pcd/** — PCD cache management (compile, watch, query)
|
|
- **jobs/** — Background job scheduler and definitions
|
|
- **sync/** — Logic for pushing configs to Arr instances
|
|
- **upgrades/** — Automatic quality upgrade processing
|
|
- **notifications/** — Discord/Slack/Email delivery
|
|
- **utils/** — Shared utilities (arr clients, git, http, logger, config, cache)
|
|
|
|
#### Utils
|
|
|
|
Shared utilities live in `src/lib/server/utils/`. These are foundational modules
|
|
used throughout the backend.
|
|
|
|
**Config**
|
|
|
|
The config singleton (`config/config.ts`) is the most important utility. It
|
|
centralizes all application paths and environment configuration. Import it via
|
|
`$config`.
|
|
|
|
```typescript
|
|
import { config } from '$config';
|
|
|
|
// Paths
|
|
config.paths.base; // Application root
|
|
config.paths.logs; // Log directory
|
|
config.paths.data; // Data directory
|
|
config.paths.database; // SQLite database file
|
|
config.paths.databases; // PCD repositories directory
|
|
config.paths.backups; // Backup directory
|
|
|
|
// Server
|
|
config.port; // HTTP port (default: 6868)
|
|
config.host; // Bind address (default: 0.0.0.0)
|
|
config.serverUrl; // Display URL (http://localhost:6868)
|
|
|
|
// Services
|
|
config.parserUrl; // Parser microservice URL
|
|
config.timezone; // System timezone
|
|
```
|
|
|
|
The base path defaults to the executable's directory but can be overridden via
|
|
`APP_BASE_PATH`. Call `config.init()` on startup to create required directories.
|
|
|
|
**Logger**
|
|
|
|
The logger (`logger/logger.ts`) handles console and file output with daily
|
|
rotation. Import via `$logger/logger.ts`.
|
|
|
|
```typescript
|
|
import { logger } from '$logger/logger.ts';
|
|
|
|
await logger.debug('Cache miss', { source: 'PCD', meta: { id: 1 } });
|
|
await logger.info('Sync completed', { source: 'Sync' });
|
|
await logger.warn('Rate limited', { source: 'GitHub' });
|
|
await logger.error('Connection failed', { source: 'Arr', meta: error });
|
|
```
|
|
|
|
Log levels: DEBUG → INFO → WARN → ERROR. Users configure the minimum level in
|
|
settings. File logs are JSON (one entry per line), console logs are colored.
|
|
|
|
**Logging guidelines:**
|
|
|
|
- **DEBUG** — Internal state, cache hits/misses, detailed flow. Developers only.
|
|
Use liberally during development but ensure production logs aren't flooded.
|
|
- **INFO** — User-relevant events: sync completed, backup created, job finished.
|
|
Think of these as feedback for the user, similar to alerts in the frontend.
|
|
Keep them concise and actionable.
|
|
- **WARN** — Recoverable issues: rate limits, missing optional config, fallback
|
|
behavior triggered.
|
|
- **ERROR** — Failures requiring attention: connection errors, invalid data,
|
|
unhandled exceptions.
|
|
|
|
Good logs are concise and contextual. Include `source` to identify the
|
|
subsystem. Include `meta` for structured data that helps debugging. Avoid
|
|
verbose messages or logging the same event multiple times.
|
|
|
|
```typescript
|
|
// Good
|
|
await logger.info('Synced 5 profiles to Radarr', { source: 'Sync' });
|
|
|
|
// Bad - too verbose, no source
|
|
await logger.info('Starting to sync profiles now...');
|
|
await logger.info('Found 5 profiles to sync');
|
|
await logger.info('Syncing profile 1...');
|
|
await logger.info('Syncing profile 2...');
|
|
```
|
|
|
|
**HTTP**
|
|
|
|
The HTTP client (`http/client.ts`) provides a base class with connection pooling
|
|
and retry logic. Arr clients extend this.
|
|
|
|
```typescript
|
|
import { BaseHttpClient } from '$http/client.ts';
|
|
|
|
class MyClient extends BaseHttpClient {
|
|
constructor(url: string) {
|
|
super(url, {
|
|
timeout: 30000, // Request timeout (ms)
|
|
retries: 3, // Retry count for 5xx errors
|
|
retryDelay: 500, // Base delay (exponential backoff)
|
|
headers: { Authorization: 'Bearer token' }
|
|
});
|
|
}
|
|
}
|
|
|
|
const client = new MyClient('https://api.example.com');
|
|
const data = await client.get<Response>('/endpoint');
|
|
client.close(); // Release connection pool
|
|
```
|
|
|
|
Features:
|
|
|
|
- Connection pooling via `Deno.createHttpClient()`
|
|
- Automatic retries with exponential backoff for 500/502/503/504
|
|
- Configurable timeout with AbortController
|
|
- JSON request/response handling
|
|
- `HttpError` class with status code and response body
|
|
|
|
Always call `close()` when done to release pooled connections.
|
|
|
|
**Git**
|
|
|
|
Git operations (`git/`) wrap command-line git for PCD repository management.
|
|
|
|
The `Git` class (`Git.ts`) provides a clean interface per repository:
|
|
|
|
```typescript
|
|
import { Git } from '$utils/git/index.ts';
|
|
|
|
const git = new Git('/path/to/repo');
|
|
|
|
// Repository operations
|
|
await git.fetch();
|
|
await git.pull();
|
|
await git.push();
|
|
await git.checkout('main');
|
|
await git.resetToRemote();
|
|
|
|
// Status queries
|
|
const branch = await git.getBranch();
|
|
const status = await git.status();
|
|
const updates = await git.checkForUpdates();
|
|
const commits = await git.getCommits(10);
|
|
|
|
// PCD operation files
|
|
const uncommitted = await git.getUncommittedOps();
|
|
const maxOp = await git.getMaxOpNumber();
|
|
await git.discardOps(filepaths);
|
|
await git.addOps(filepaths, 'commit message');
|
|
```
|
|
|
|
Key modules:
|
|
|
|
- `repo.ts` — Clone, fetch, pull, push, checkout, reset, stage, commit
|
|
- `status.ts` — Branch info, status, update checks, commit history, diffs
|
|
- `ops.ts` — PCD-specific operations: parse operation metadata, get uncommitted
|
|
ops, renumber and commit ops
|
|
|
|
The `clone()` function in `repo.ts` validates GitHub URLs via API before
|
|
cloning, detects private repositories, and handles PAT authentication.
|
|
|
|
**Cache**
|
|
|
|
Simple in-memory cache with TTL (`cache/cache.ts`):
|
|
|
|
```typescript
|
|
import { cache } from '$cache/cache.ts';
|
|
|
|
cache.set('key', data, 300); // TTL in seconds
|
|
const value = cache.get<Type>('key'); // Returns undefined if expired
|
|
cache.delete('key');
|
|
cache.deleteByPrefix('library:'); // Clear related entries
|
|
cache.clear();
|
|
```
|
|
|
|
Used for expensive computations like library data fetching. Not persisted across
|
|
restarts.
|
|
|
|
**AI**
|
|
|
|
Optional AI integration (`ai/client.ts`) for generating commit messages from
|
|
diffs. Supports OpenAI-compatible APIs including local models.
|
|
|
|
```typescript
|
|
import { isAIEnabled, generateCommitMessage } from '$utils/ai/client.ts';
|
|
|
|
if (isAIEnabled()) {
|
|
const message = await generateCommitMessage(diffText);
|
|
}
|
|
```
|
|
|
|
Configured via settings UI (API URL, model, optional API key). Uses Chat
|
|
Completions API for most models, Responses API for GPT-5.
|
|
|
|
#### Main Database
|
|
|
|
SQLite database in `src/lib/server/db/`. No ORM—raw SQL with typed wrappers.
|
|
|
|
Migrations live in `migrations/` as numbered TypeScript files. Each exports a
|
|
`migration` object with `version`, `name`, `up` (SQL), and optional `down`. New
|
|
migrations must be imported and added to the array in `migrations.ts`, and
|
|
`schema.sql` must be updated to reflect the current schema. Migrations run
|
|
automatically on startup in order.
|
|
|
|
Examples:
|
|
|
|
- `src/lib/server/db/migrations/001_create_arr_instances.ts`
|
|
- `src/lib/server/db/migrations/007_create_notification_tables.ts`
|
|
- `src/lib/server/db/schema.sql`
|
|
|
|
All queries live in `queries/`, one file per table. Each file exports a query
|
|
object (e.g., `arrInstancesQueries`) with typed methods for CRUD operations.
|
|
**Queries are not written anywhere else in the codebase**—route handlers and
|
|
other code import from `queries/` rather than writing SQL inline.
|
|
|
|
Examples:
|
|
|
|
- `src/lib/server/db/queries/arrInstances.ts`
|
|
- `src/lib/server/db/queries/jobs.ts`
|
|
|
|
**Date Handling**
|
|
|
|
SQLite stores timestamps from `CURRENT_TIMESTAMP` as UTC in the format
|
|
`"YYYY-MM-DD HH:MM:SS"` without a timezone indicator. JavaScript's `Date`
|
|
constructor interprets strings without timezone info as local time, causing
|
|
incorrect display.
|
|
|
|
Use the shared date utilities in `src/lib/shared/dates.ts`:
|
|
|
|
```typescript
|
|
import { parseUTC, toUTC } from '$shared/dates';
|
|
|
|
// Parse SQLite timestamp to Date object (correctly interpreted as UTC)
|
|
const date = parseUTC('2026-01-17 03:21:52'); // Date in UTC
|
|
|
|
// Normalize to ISO 8601 string with Z suffix
|
|
const iso = toUTC('2026-01-17 03:21:52'); // "2026-01-17T03:21:52Z"
|
|
```
|
|
|
|
**Never** manually append `'Z'` or manipulate timestamp strings directly. Always
|
|
use these utilities to ensure consistent timezone handling across the codebase.
|
|
|
|
For SQL queries that compare timestamps, normalize both sides:
|
|
|
|
```sql
|
|
datetime(replace(replace(timestamp_col, 'T', ' '), 'Z', '')) <= datetime('now')
|
|
```
|
|
|
|
#### PCD (Profilarr Compliant Database)
|
|
|
|
PCDs are git repositories containing versioned configuration data—quality
|
|
profiles, custom formats, delay profiles, media management settings, and regular
|
|
expressions. Unlike the main database which stores application state directly,
|
|
PCDs store _operations_: append-only SQL files that are replayed to build an
|
|
in-memory database. This design enables git-based versioning, conflict-free
|
|
merging, and layered customization.
|
|
|
|
Every PCD depends on a shared schema repository
|
|
([github.com/Dictionarry-Hub/schema](https://github.com/Dictionarry-Hub/schema))
|
|
that defines the base tables. The official database is
|
|
[github.com/Dictionarry-Hub/db](https://github.com/Dictionarry-Hub/db).
|
|
|
|
**Operational SQL (OSQL)**
|
|
|
|
PCDs use an append-only approach where each change is written as a numbered SQL
|
|
file. Instead of mutating rows directly, you append INSERT, UPDATE, or DELETE
|
|
statements. When the cache compiles, it replays all operations in order to build
|
|
the current state. This makes every change trackable in git history and enables
|
|
non-destructive layering.
|
|
|
|
**Layers**
|
|
|
|
Operations are loaded and executed in a specific order:
|
|
|
|
1. **Schema** (`deps/schema/ops/`) — Table definitions and seed data from the
|
|
schema dependency. Creates the database structure.
|
|
|
|
2. **Base** (`ops/`) — The PCD's main configuration data. Quality profiles,
|
|
custom formats, and other entities maintained by the database author.
|
|
|
|
3. **Tweaks** (`tweaks/`) — Optional adjustments that apply on top of base.
|
|
Useful for variant configurations or environment-specific overrides.
|
|
|
|
4. **User** (`user_ops/`) — Local modifications made by the end user. These stay
|
|
on the user's machine and persist across pulls from upstream.
|
|
|
|
Files within each layer are sorted by numeric prefix (`1.initial.sql`,
|
|
`2.add-formats.sql`, etc.) and executed in order.
|
|
|
|
**Repository Layout**
|
|
|
|
```
|
|
my-pcd/
|
|
├── pcd.json # Manifest file
|
|
├── ops/ # Base layer operations
|
|
│ ├── 1.initial.sql
|
|
│ └── 2.custom-formats.sql
|
|
├── deps/
|
|
│ └── schema/ # Schema dependency (git submodule)
|
|
│ └── ops/
|
|
├── tweaks/ # Optional tweaks layer
|
|
└── user_ops/ # User modifications (gitignored)
|
|
```
|
|
|
|
**Manifest**
|
|
|
|
Every PCD requires a `pcd.json` manifest:
|
|
|
|
```json
|
|
{
|
|
"name": "my-database",
|
|
"version": "1.0.0",
|
|
"description": "Custom Arr configurations",
|
|
"dependencies": {
|
|
"https://github.com/Dictionarry-Hub/schema": "main"
|
|
},
|
|
"arr_types": ["radarr", "sonarr"],
|
|
"profilarr": {
|
|
"minimum_version": "2.0.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Cache Compilation**
|
|
|
|
When Profilarr loads a PCD, it creates an in-memory SQLite database and replays
|
|
all operations in layer order. The `PCDCache` class in
|
|
`src/lib/server/pcd/cache.ts` handles this:
|
|
|
|
1. Creates an in-memory SQLite database
|
|
2. Registers helper functions (`qp()`, `cf()`, `dp()`, `tag()`) for entity
|
|
lookups
|
|
3. Loads operations from all layers via `loadAllOperations()`
|
|
4. Executes each SQL file in order
|
|
5. Exposes the compiled database through Kysely for type-safe queries
|
|
|
|
File watchers monitor the ops directories. When a `.sql` file changes, the cache
|
|
automatically recompiles after a short debounce.
|
|
|
|
**Writing Operations**
|
|
|
|
When users edit entities through the frontend, changes are not applied directly
|
|
to the in-memory cache. Instead, `src/lib/server/pcd/writer.ts` generates SQL
|
|
files and writes them to the appropriate layer:
|
|
|
|
- **Base layer** (`ops/`) — For database maintainers with push access. Requires
|
|
a personal access token.
|
|
- **User layer** (`user_ops/`) — For local modifications. No authentication
|
|
required.
|
|
|
|
The writer converts Kysely queries to executable SQL, assigns the next sequence
|
|
number, and writes the file. After writing, it triggers a cache recompile so
|
|
changes appear immediately.
|
|
|
|
```typescript
|
|
// Example: writer converts this Kysely query to a .sql file
|
|
await writeOperation({
|
|
databaseId: 1,
|
|
layer: 'user',
|
|
description: 'update-profile-score',
|
|
queries: [compiledKyselyQuery],
|
|
metadata: {
|
|
operation: 'update',
|
|
entity: 'quality_profile',
|
|
name: 'HD Bluray + WEB'
|
|
}
|
|
});
|
|
```
|
|
|
|
This writes `user_ops/5.update-profile-score.sql` with the SQL and metadata
|
|
header, then recompiles the cache.
|
|
|
|
**Queries**
|
|
|
|
PCD queries live in `src/lib/server/pcd/queries/`, organized by entity type.
|
|
Each query file exports functions that use the `PCDCache` instance to read
|
|
compiled data:
|
|
|
|
- `src/lib/server/pcd/queries/qualityProfiles/` — List, get, create, update
|
|
- `src/lib/server/pcd/queries/customFormats/` — List, get, conditions, tests
|
|
- `src/lib/server/pcd/queries/delayProfiles/`
|
|
- `src/lib/server/pcd/queries/regularExpressions/`
|
|
- `src/lib/server/pcd/queries/mediaManagement/`
|
|
|
|
#### Sync
|
|
|
|
The sync module (`src/lib/server/sync/`) pushes compiled PCD configurations to
|
|
Arr instances. It reads from the PCD cache, transforms data to match each Arr's
|
|
API format, and creates or updates entities by name.
|
|
|
|
**Architecture**
|
|
|
|
Syncers extend `BaseSyncer`, which provides a fetch → transform → push pattern:
|
|
|
|
1. **Fetch** — Read entities from the PCD cache
|
|
2. **Transform** — Convert PCD data to Arr API format using transformers
|
|
3. **Push** — Create or update entities in the Arr instance (matched by name)
|
|
|
|
Three syncer implementations handle different entity types:
|
|
|
|
- `QualityProfileSyncer` — Syncs quality profiles and their dependent custom
|
|
formats. Custom formats sync first so profile references resolve correctly.
|
|
- `DelayProfileSyncer` — Syncs delay profiles with protocol preferences and
|
|
bypass settings.
|
|
- `MediaManagementSyncer` — Syncs naming conventions, quality definitions, and
|
|
media settings.
|
|
|
|
**Triggers**
|
|
|
|
Syncs are triggered by `should_sync` flags in the main database. The processor
|
|
evaluates these flags and runs appropriate syncers:
|
|
|
|
- **Manual** — User clicks "Sync Now" in the UI
|
|
- **on_pull** — Triggered after pulling updates from a database repository
|
|
- **on_change** — Triggered when PCD files change (detected by file watcher)
|
|
- **schedule** — Cron expressions evaluated periodically; marks configs for sync
|
|
when the schedule matches
|
|
|
|
The `processPendingSyncs()` function in `processor.ts` orchestrates all pending
|
|
syncs, iterating through flagged instances and running the appropriate syncers.
|
|
|
|
**Transformers**
|
|
|
|
Transformers in `transformers/` convert PCD data structures to Arr API payloads.
|
|
They handle differences between Radarr and Sonarr APIs:
|
|
|
|
- `customFormat.ts` — Transforms custom format conditions to API specifications.
|
|
Maps condition types (release_title, source, resolution) to their API
|
|
implementations and converts values using mappings.
|
|
- `qualityProfile.ts` — Transforms quality tiers, language settings, and format
|
|
scores. Handles quality name differences between apps.
|
|
|
|
**Mappings**
|
|
|
|
`mappings.ts` contains constants for translating between PCD values and Arr API
|
|
values. This includes indexer flags, sources, resolutions, quality definitions,
|
|
and languages. Each constant has separate mappings for Radarr and Sonarr where
|
|
their APIs differ.
|
|
|
|
#### Jobs
|
|
|
|
The job system (`src/lib/server/jobs/`) runs background tasks on schedules. Jobs
|
|
handle recurring operations like syncing databases, creating backups, cleaning
|
|
up logs, and processing upgrades.
|
|
|
|
**Components**
|
|
|
|
- **Registry** (`registry.ts`) — Stores job definitions in memory. Jobs register
|
|
on startup and can be looked up by name.
|
|
- **Scheduler** (`scheduler.ts`) — Checks for due jobs every minute and triggers
|
|
execution. Prevents concurrent runs of the same check cycle.
|
|
- **Runner** (`runner.ts`) — Executes a job's handler, records the run in the
|
|
database, calculates the next run time, and sends notifications on
|
|
success/failure.
|
|
- **Init** (`init.ts`) — Registers all job definitions and syncs them with the
|
|
database on startup.
|
|
|
|
**Defining Jobs**
|
|
|
|
Job definitions live in `definitions/`. Each exports a `JobDefinition` with
|
|
name, description, schedule (cron expression), and handler function:
|
|
|
|
```typescript
|
|
export const myJob: JobDefinition = {
|
|
name: 'my_job',
|
|
description: 'Does something useful',
|
|
schedule: '0 * * * *', // Every hour
|
|
handler: async (): Promise<JobResult> => {
|
|
// Job logic here
|
|
return { success: true, output: 'Done' };
|
|
}
|
|
};
|
|
```
|
|
|
|
Register the job in `init.ts` by importing and calling
|
|
`jobRegistry.register(myJob)`.
|
|
|
|
**Built-in Jobs**
|
|
|
|
- `sync_arr` — Processes pending syncs to Arr instances (every minute)
|
|
- `sync_databases` — Pulls updates from linked database repositories
|
|
- `create_backup` — Creates application backups
|
|
- `cleanup_backups` — Removes old backups based on retention settings
|
|
- `cleanup_logs` — Prunes old log entries
|
|
- `upgrade_manager` — Processes automatic quality upgrades
|
|
|
|
**Job Logic**
|
|
|
|
Complex job logic lives in `logic/`. Definition files stay thin—they just wire
|
|
up the handler to the logic function. This keeps definitions readable and logic
|
|
testable.
|
|
|
|
#### Notifications
|
|
|
|
The notification system (`src/lib/server/notifications/`) sends alerts to
|
|
external services like Discord. It's fire-and-forget: failures are logged but
|
|
don't interrupt the calling code.
|
|
|
|
**Components**
|
|
|
|
- **NotificationManager** (`NotificationManager.ts`) — Central orchestrator.
|
|
Queries enabled services from the database, filters by notification type, and
|
|
dispatches to appropriate notifiers. Records all attempts in history.
|
|
- **Builder** (`builder.ts`) — Fluent API for constructing notifications. Chain
|
|
`.generic()` for fallback content, `.discord()` for rich embeds, and call
|
|
`.send()` to dispatch.
|
|
- **Definitions** (`definitions/`) — Pre-built notification factories for common
|
|
events. Import from `definitions/index.ts` and call with parameters.
|
|
- **Notifiers** (`notifiers/`) — Service-specific implementations. Currently
|
|
only Discord is implemented in `notifiers/discord/`.
|
|
|
|
**Usage**
|
|
|
|
For simple notifications, use the builder directly:
|
|
|
|
```typescript
|
|
import { notify } from '$notifications/builder.ts';
|
|
|
|
await notify('pcd.sync_success').generic('Sync Complete', 'Synced 5 profiles to Radarr').send();
|
|
```
|
|
|
|
For rich Discord notifications with embeds:
|
|
|
|
```typescript
|
|
import { notify, createEmbed, Colors } from '$notifications/builder.ts';
|
|
|
|
await notify('rename.success')
|
|
.generic('Rename Complete', 'Renamed 5 files')
|
|
.discord((d) =>
|
|
d.embed(
|
|
createEmbed()
|
|
.title('Rename Complete')
|
|
.description('All files renamed successfully')
|
|
.field('Files', '5/5', true)
|
|
.field('Mode', 'Live', true)
|
|
.color(Colors.SUCCESS)
|
|
.timestamp()
|
|
)
|
|
)
|
|
.send();
|
|
```
|
|
|
|
For complex notifications, create a definition in `definitions/`:
|
|
|
|
```typescript
|
|
// definitions/myFeature.ts
|
|
import { notify, createEmbed, Colors } from '../builder.ts';
|
|
|
|
interface MyNotificationParams {
|
|
log: MyJobLog;
|
|
config: { username?: string };
|
|
}
|
|
|
|
export const myFeature = ({ log, config }: MyNotificationParams) =>
|
|
notify(`myfeature.${log.status}`)
|
|
.generic('Feature Complete', `Processed ${log.count} items`)
|
|
.discord((d) =>
|
|
d.embed(
|
|
createEmbed()
|
|
.title('Feature Complete')
|
|
.field('Processed', String(log.count), true)
|
|
.color(log.status === 'success' ? Colors.SUCCESS : Colors.ERROR)
|
|
.timestamp()
|
|
)
|
|
);
|
|
|
|
// Usage:
|
|
import { notifications } from '$notifications/definitions/index.ts';
|
|
await notifications.myFeature({ log, config }).send();
|
|
```
|
|
|
|
**Discord Embed Builder**
|
|
|
|
The embed builder (`notifiers/discord/embed.ts`) provides a fluent API:
|
|
|
|
```typescript
|
|
createEmbed()
|
|
.author('Profilarr', iconUrl)
|
|
.title('Title')
|
|
.description('Description text')
|
|
.field('Name', 'Value', true) // inline field
|
|
.fieldIf(condition, 'Name', 'Value') // conditional field
|
|
.color(Colors.SUCCESS)
|
|
.timestamp()
|
|
.footer('Profilarr')
|
|
.build();
|
|
```
|
|
|
|
Available colors: `Colors.SUCCESS`, `Colors.ERROR`, `Colors.WARNING`,
|
|
`Colors.INFO`, `Colors.PREVIEW`.
|
|
|
|
Instance icons: `Icons.RADARR`, `Icons.SONARR`, `Icons.LIDARR`, `Icons.READARR`.
|
|
|
|
The Discord notifier automatically handles:
|
|
|
|
- Splitting large notifications across multiple messages (1 embed per message)
|
|
- Rate limiting between messages (1 second delay)
|
|
- Fallback to generic content when no Discord-specific payload is provided
|
|
|
|
**Notification Types**
|
|
|
|
Types are defined in two places:
|
|
|
|
- `src/lib/server/notifications/types.ts` — Backend constants
|
|
- `src/lib/shared/notificationTypes.ts` — Shared definitions with labels and
|
|
descriptions for the settings UI
|
|
|
|
Current types:
|
|
|
|
- `job.<name>.success` / `job.<name>.failed` — Job completion status
|
|
- `pcd.linked` / `pcd.unlinked` — Database connection changes
|
|
- `pcd.sync_success` / `pcd.sync_failed` — Sync results
|
|
- `upgrade.success` / `upgrade.partial` / `upgrade.failed` — Upgrade results
|
|
- `rename.success` / `rename.partial` / `rename.failed` — Rename results
|
|
|
|
When adding a new notification type, add it to both files. Users configure which
|
|
types each service receives in the settings UI.
|
|
|
|
**Planned Services**
|
|
|
|
Currently only Discord is implemented. Planned additions:
|
|
|
|
- Telegram
|
|
- Slack
|
|
- Ntfy
|
|
- Apprise
|
|
- SMTP (email)
|
|
- Generic webhooks
|
|
|
|
**Adding Notifiers**
|
|
|
|
To add a new notification service:
|
|
|
|
1. Create a config interface in `types.ts` (e.g., `SlackConfig`)
|
|
2. Add the service type to `NotificationServiceType` union
|
|
3. Create a notifier directory in `notifiers/` (e.g., `notifiers/slack/`)
|
|
4. Implement a notifier class with a `notify(notification: Notification)` method
|
|
5. Add the case to `NotificationManager.createNotifier()`
|
|
6. Create frontend configuration component in
|
|
`src/routes/settings/notifications/components/`
|
|
|
|
#### Arr Clients
|
|
|
|
The arr utilities (`src/lib/server/utils/arr/`) provide typed HTTP clients for
|
|
communicating with Radarr, Sonarr, Lidarr, and Chaptarr instances.
|
|
|
|
**Base Client**
|
|
|
|
`BaseArrClient` extends `BaseHttpClient` with arr-specific methods: connection
|
|
testing, delay profiles, tags, media management config, naming config, quality
|
|
definitions, custom formats, and quality profiles. All arr clients inherit from
|
|
this base.
|
|
|
|
**App-Specific Clients**
|
|
|
|
Each arr has its own client in `clients/` that extends `BaseArrClient` with
|
|
app-specific functionality:
|
|
|
|
- `RadarrClient` — Adds movie operations, library fetching with computed custom
|
|
format scores, search commands, and tag management.
|
|
- `SonarrClient` — Series and episode operations.
|
|
- `LidarrClient` — Artist and album operations.
|
|
- `ChaptarrClient` — Chapter-specific operations.
|
|
|
|
**Factory**
|
|
|
|
`createArrClient(type, url, apiKey)` returns the appropriate client instance
|
|
based on the arr type. Used throughout the codebase when interacting with arr
|
|
instances.
|
|
|
|
**Library Browser**
|
|
|
|
The library browser (`src/routes/arr/[id]/library/`) displays downloaded media
|
|
with computed custom format scores and cutoff progress.
|
|
|
|
**Supported:** Radarr only. **TODO:** Sonarr library views.
|
|
|
|
The page fetches library data via API, which calls `RadarrClient.getLibrary()`.
|
|
This pulls movies, quality profiles, and movie files in parallel, then computes:
|
|
|
|
- **Custom format score** — Sum of matched format scores from the profile
|
|
- **Cutoff progress** — Score as percentage of cutoff (0% to 100%+)
|
|
- **Score breakdown** — Individual format contributions shown on row expand
|
|
|
|
Features:
|
|
|
|
- **Filtering** — Filter by quality name or profile. Multiple filters use OR
|
|
within the same field, AND across fields.
|
|
- **Search** — Debounced title search.
|
|
- **Column visibility** — Toggle columns on/off, persisted to localStorage.
|
|
- **Profilarr profile detection** — Movies using profiles synced from Profilarr
|
|
databases show a blue badge; others show amber with a warning icon.
|
|
- **Expandable rows** — Click a row to see filename and score breakdown with
|
|
each format's contribution color-coded (green positive, red negative).
|
|
- **Client-side caching** — Library data cached per instance to avoid refetching
|
|
on navigation. Refresh button clears cache and refetches.
|
|
|
|
#### Upgrades
|
|
|
|
The upgrade system (`src/lib/server/upgrades/`) solves a fundamental limitation
|
|
of how Radarr and Sonarr work. The arrs don't search for the _best_ release—they
|
|
monitor RSS feeds and grab the first thing that qualifies as an upgrade. To
|
|
actually get optimal releases, you need to trigger manual searches.
|
|
|
|
Profilarr's upgrade module automates this with configurable filters and
|
|
selectors, similar to
|
|
[Upgradinatorr](https://github.com/angrycuban13/Just-A-Bunch-Of-Starr-Scripts/blob/main/Upgradinatorr/README.md)
|
|
but built directly into the app.
|
|
|
|
**Shared Types**
|
|
|
|
Filter and selector logic lives in `src/lib/shared/` so both frontend and
|
|
backend use the same definitions:
|
|
|
|
- `filters.ts` — Filter field definitions (monitored, cutoff_met, year,
|
|
popularity, tmdb_rating, etc.), operators (boolean, number, text, date),
|
|
rule/group types, and the `evaluateGroup()` function that recursively
|
|
evaluates nested AND/OR logic.
|
|
- `selectors.ts` — Selector definitions (random, oldest, newest, lowest_score,
|
|
most_popular, least_popular) with their `select()` functions. Each selector
|
|
sorts/shuffles items and returns the top N.
|
|
|
|
**Processing Flow**
|
|
|
|
The upgrade processor (`processor.ts`) orchestrates each run:
|
|
|
|
1. **Fetch** — Pull the entire library from the arr instance along with quality
|
|
profiles and movie files.
|
|
2. **Normalize** — Convert arr data to a flat structure with fields matching
|
|
filter rule names (`monitored`, `cutoff_met`, `size_on_disk`, `tmdb_rating`,
|
|
`popularity`, etc.).
|
|
3. **Filter** — Call `evaluateGroup()` from `$shared/filters.ts` to evaluate
|
|
rules using AND/OR group logic. Supports nested groups and operators
|
|
appropriate to each field type.
|
|
4. **Cooldown** — Remove items that were searched recently. The system uses
|
|
date-based tags (e.g., `profilarr-searched-2026-01-15`) to track when items
|
|
were last searched.
|
|
5. **Select** — Call `getSelector()` from `$shared/selectors.ts` to pick which
|
|
items get searched. Options: random, oldest, newest, lowest CF score, most
|
|
popular, least popular.
|
|
6. **Search** — Trigger searches via the arr's command API. Tag searched items
|
|
with today's date for cooldown tracking.
|
|
|
|
**Filter Modes**
|
|
|
|
When multiple filters are configured:
|
|
|
|
- **Round Robin** — Cycles through filters in order, one filter per scheduled
|
|
run. Filter index persists across runs.
|
|
- **Random** — Picks a random enabled filter each run.
|
|
|
|
**Dry Run**
|
|
|
|
Configs can be set to dry run mode, which executes the full filter/select
|
|
pipeline but skips the actual search and tagging. Useful for testing filter
|
|
logic before enabling real searches.
|
|
|
|
**Structured Logging**
|
|
|
|
Each upgrade run produces an `UpgradeJobLog` with detailed metrics: library
|
|
size, filter match counts, cooldown effects, selection details, search results.
|
|
The logger (`logger.ts`) formats these for the application log.
|
|
|
|
**Rename (TODO)**
|
|
|
|
A future rename module will use the same architecture but simpler: instead of
|
|
triggering searches, it will trigger rename commands for items matching filters.
|
|
Same filter/select flow, different action.
|
|
|
|
### Microservices
|
|
|
|
#### Parser
|
|
|
|
A C# parser module lives in `src/services/parser`. This is a direct port of
|
|
Radarr/Sonarr's parsing logic, packaged under a single unified endpoint that
|
|
Profilarr uses for its testing functionality. It runs as a separate service and
|
|
communicates with the main app over HTTP.
|
|
|
|
- **.NET 8.0** (`net8.0`)
|
|
|
|
### API
|
|
|
|
API routes live in `src/routes/api/`. The API is documented using OpenAPI 3.1
|
|
specification in `docs/api/v1/`.
|
|
|
|
**Documentation Requirement**
|
|
|
|
When adding a new API endpoint, you must document it in the OpenAPI spec. This
|
|
is not optional. The spec serves as the source of truth for API consumers and
|
|
generates TypeScript types via `deno task generate:api-types`.
|
|
|
|
**Spec Structure**
|
|
|
|
```
|
|
docs/api/v1/
|
|
├── openapi.yaml # Main spec file, references paths and schemas
|
|
├── paths/ # Endpoint definitions grouped by domain
|
|
│ └── system.yaml # Example: health, openapi endpoints
|
|
└── schemas/ # Reusable type definitions
|
|
├── common.yaml # Shared types (ComponentStatus, etc.)
|
|
└── health.yaml # Domain-specific types
|
|
```
|
|
|
|
**Adding an Endpoint**
|
|
|
|
1. Create or update a path file in `docs/api/v1/paths/`:
|
|
|
|
```yaml
|
|
# paths/databases.yaml
|
|
list:
|
|
get:
|
|
operationId: listDatabases
|
|
summary: List all databases
|
|
description: Returns all linked PCD databases
|
|
tags:
|
|
- Databases
|
|
responses:
|
|
'200':
|
|
description: List of databases
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '../schemas/database.yaml#/Database'
|
|
```
|
|
|
|
2. Reference it in `openapi.yaml`:
|
|
|
|
```yaml
|
|
paths:
|
|
/databases:
|
|
$ref: './paths/databases.yaml#/list'
|
|
```
|
|
|
|
3. Add any new schemas to `schemas/`:
|
|
|
|
```yaml
|
|
# schemas/database.yaml
|
|
Database:
|
|
type: object
|
|
required:
|
|
- id
|
|
- name
|
|
- repositoryUrl
|
|
properties:
|
|
id:
|
|
type: integer
|
|
name:
|
|
type: string
|
|
repositoryUrl:
|
|
type: string
|
|
format: uri
|
|
```
|
|
|
|
4. Run `deno task generate:api-types` to regenerate TypeScript types.
|
|
|
|
**Route Conventions**
|
|
|
|
- Return JSON with consistent shapes:
|
|
- Success: `{ success: true, data?: ... }` or just the data
|
|
- Error: `{ success: false, error: "message" }`
|
|
- Use appropriate status codes: 200 OK, 201 Created, 400 Bad Request, 404 Not
|
|
Found, 500 Internal Server Error
|
|
- Validate input early with guard clauses
|
|
- Wrap operations in try-catch, return 500 with error message for unexpected
|
|
failures
|
|
|
|
**Viewing Docs**
|
|
|
|
The OpenAPI spec is served at `/api/v1/openapi.json` when the app is running.
|
|
You can load this into Swagger UI or other OpenAPI tools to browse the API
|
|
interactively
|