feat: source management REST API — register, sync, delete + tests (tasks 6.1-6.4)#7
Conversation
Add POST /api/v1/sources, DELETE /api/v1/sources/:id, and POST /api/v1/sources/:id/sync endpoints with full validation, error handling, atomic cleanup on clone failure, concurrent sync guard, and 11 API tests. Co-authored-by: Cursor <cursoragent@cursor.com>
…ks 5.1-5.3, 5.5-5.6)
Add GET /api/v1/skills (paginated listing), GET /api/v1/skills/search
(Fuse.js fuzzy search), and GET /api/v1/skills/:source/:slug (full skill
detail) endpoints. Implement FuseSearchProvider backed by Fuse.js with
index rebuild wired into the server. Add global Fastify error handler
returning structured {error: {code, message}} for all 4xx/5xx. Guard
top-level buildServer() call so index.ts is safe to import in tests.
Add 17 API tests covering pagination bounds, 404 on unknown skill, and
fuzzy search. Mark tasks 5.1-5.3, 5.5-5.7 complete.
Co-authored-by: Cursor <cursoragent@cursor.com>
…sponse - POST /api/v1/sources now clones before writing to DB so a clone failure never creates an orphaned source record (spec: 'no source record is created') - Response field renamed createdAt → created_at to match spec contract Co-authored-by: Cursor <cursoragent@cursor.com>
PR Summary by QodoImplement sources CRUD/sync API, skills catalog endpoints, and Fuse.js search Description
Diagram
High-Level Assessment
Files changed (9)
|
Code Review by Qodo
1.
|
…ic sync, timestamps, ingest label, hermetic test
- findAllUnpaged(): new SkillRepository method for index builds; bypasses the
100-row public pagination cap that made findAll({perPage:10000}) silently
incomplete. Used in startup and every rebuildSearchIndex() call.
- id in SearchIndexItem/SearchResult: embed the skill DB id directly in the
Fuse index so the search route no longer does a secondary DB lookup per hit
(which could return undefined when the index is stale). Both search types.ts
and FuseSearchProvider updated.
- Archive detail: GET /skills/:source/:slug now correctly expands archive-type
skills (base64 tar.gz) into files:[{path,contents}] on demand instead of
returning the raw base64 blob as SKILL.md content.
- Atomic sync guard: replace non-atomic check-then-set with
trySetSyncing() — a single SQL UPDATE...WHERE sync_status != 'syncing' that
returns the affected row count. Two concurrent requests can no longer both
win the race and start ingestion.
- Sync timestamps: updateSync now uses a CASE expression so last_synced_at is
only written on successful completion (status='idle'); entering 'syncing' or
transitioning to 'error' no longer overwrites or clears the last good timestamp.
- INGEST_FAILED: ingestion failures after a successful clone now return
{code:"INGEST_FAILED"} instead of CLONE_FAILED, making client error handling
and alerting unambiguous.
- Hermetic clone test: vi.mock() replaces the real git clone with a
deterministic rejection so the test suite doesn't require external networking.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Implements the Source Management API in
src/server/routes/sources.ts.POST /api/v1/sources— validates slug (kebab-case, 1–64 chars); 409 on duplicate; clones before writing to DB so clone failure never leaves an orphaned source record; 422CLONE_FAILEDon bad URL; 201 with source + sync report on successDELETE /api/v1/sources/:id— removes source + all associated skills (FK CASCADE); 404 on unknownPOST /api/v1/sources/:id/sync— 409SYNC_IN_PROGRESSon concurrent; returns sync report; 404 on unknownNo auth on write routes for now — Epic 3 will gate them.
Design note — clone-before-create
The registration flow clones first, only persisting the source record after a successful clone. This strictly satisfies the spec's "no source record is created" guarantee on clone failure.
Test plan
npm run build:server— zero TypeScript errorsnpm test— 74/74 passing; 11 new sources route testsRelated
Made with Cursor