CMS Kronik #04: SQLite Adapter SQLite-adapteren
Før Supabase, før PostgreSQL, havde vi brug for en indlejret database. SQLite er filbaseret, kræver ingen server, og ligger allerede i package.json. Fase 3 begyndte med arkæologi — og tre fejl, der skulle rettes.
Fase 3 er flytningen fra det lokale filsystem til rigtige databaser. Slutmålet er Supabase på Fly.io — men vi starter ikke der. Før cloud-infrastruktur, før PostgreSQL, har vi brug for noget, der kører in-process med nul konfiguration. Det er SQLite.
Filsystem-adapteren gemmer hvert dokument som en JSON-fil. SQLite gemmer dem som rækker. Udefra er API’et identisk — begge implementerer StorageAdapter. At skifte adapter er en én-linjers ændring i cms.config.ts. Det er hele pointet.
“Adapter-grænsen er den vigtigste arkitektoniske beslutning i CMS’et. Alt ovenover er adapter-uafhængigt.”
Hvad der allerede var der
Vi begyndte ikke på bar bund. Adapter-filen eksisterede. better-sqlite3 og drizzle-orm stod allerede i package.json. Nogen havde skitseret tabel-skemaet og de grundlæggende CRUD-operationer. Fase 3 begyndte med arkæologi — at læse, hvad der var der, køre testene og finde ud af, hvad der var gået i stykker.
Tabel-designet er rent: én documents-tabel med id, slug, collection, status, data (JSON-tekst), field_meta (JSON-tekst) og tidsstempler. En unik begrænsning på (collection, slug) håndhæver invarianten, at slugs er unikke pr. kollektion, ikke globalt. Den begrænsning viste sig at være et hint om den første fejl.
Tre fejl, vi rettede
Fejl 1: findBySlug ignorerede kollektionen.
Den oprindelige forespørgsel filtrerede kun på slug. Hvis du havde et pages-dokument og et posts-dokument med begge sluggen about, ville forespørgslen returnere den, der kom først. Rettelsen var én ekstra betingelse: AND(collection = ?, slug = ?). Den unikke begrænsning på tabellen håndhævede allerede dette ved skrivning — forespørgslen håndhævede det bare ikke ved læsning.
Fejl 2: findMany kunne ikke filtrere på tags.
Tags er gemt som en JSON-array inde i data-kolonnen — ["cms", "sqlite"]. SQL kender ikke til JSON-arrays som standard. SQLite gør det via json_each(). Rettelsen bruger én EXISTS-underspørgsel pr. tag:
EXISTS (
SELECT 1 FROM json_each(json_extract(data, '$.tags'))
WHERE value = ?
)
Én EXISTS-klausul pr. tag giver AND-logik: et dokument skal have alle de anmodede tags for at matche. Dette er den samme semantik, som filsystem-adapteren implementerer, nu fungerende identisk i SQLite.
“Tags filtreres med AND-logik — én EXISTS-klausul pr. tag, hver en port, dokumentet skal passere igennem.”
Fejl 3: orderBy virkede kun for topniveau-felter.
createdAt, updatedAt, slug, status — disse er kolonner. Men sortOrder, date, readTime — disse lever inde i data-JSON-blobet. Den oprindelige switch-sætning havde en default, der blot faldt tilbage til createdAt. Tavs fejl.
Rettelsen bruger json_extract med en CAST for numerisk korrekthed:
CAST(json_extract(data, '$.sortOrder') AS REAL)
At caste til REAL betyder, at 2 sorteres før 10, ikke efter. Uden castet sammenligner SQLite som strenge, og "10" < "2" leksikografisk. Filsystem-adapteren havde den samme rettelse — anvendt for måneder siden — men den var ikke nået frem til SQLite-adapteren endnu.
Test-suiten
Vi tilføjede 12 nye tests, der dækker alle tre fejlrettelser og den adfærd, der afhænger af dem:
- Kollektionsscoping —
findBySlugreturnerer det korrekte dokument, når to kollektioner deler en slug - Tags AND-logik — et dokument skal have alle de anmodede tags, ikke blot én
- Data-felt
orderBy—sortOrder: 1, 2, 3kommer tilbage i den rigtige rækkefølge _fieldMetaround-trip — AI Lock-metadata overlever en skrive-læs-cyklus- Pagination totals —
totalafspejler det filtrerede antal, ikke hele tabelstørrelsen
Det sidste er vigtigt. Hvis du forespørger findMany('posts', { status: 'published', limit: 10 }) og der er 3 udgivne indlæg ud af 50 i alt, skal total være 3 — ikke 50. Pagination-brugergrænsefladen bryder tavst sammen, hvis dette er forkert.
Resultat: 46/46 tests grønne på tværs af filsystem-, SQLite-, skema-, indholdsservice- og feltmeta-suitene.
“Det rigtige total at returnere er antallet af dokumenter, der matcher filteret — ikke antallet af alle dokumenter i kollektionen.”
Hvad det muliggør
Med SQLite-adapteren solidt på plads bliver nogle ting virkelige:
- Admin-brugergrænsefladen kan skrive. Filsystem-skriver fungerer fint lokalt, men SQLite er fundamentet for en ordentlig skrivevej med samtidig adgang.
- Adapter-paritet er verificeret. Enhver adfærd, som filsystem-adapteren har, har SQLite nu også. Samme fejl, samme rettelser, samme tests.
- Vejen til Supabase er klar. Fase 3.4 bytter
SqliteStorageAdapterud medSupabaseStorageAdapter. Kollektionerne, forespørgsels-API’et, feltmeta’et — intet af det ændrer sig.
Næste skridt: GitHub-adapteren (indhold som git-objekter) og derefter Supabase. Filsystemet var Fase 1. SQLite er Fase 3.0. Hver adapter er et trin på den samme stige.