@airdraft/db-adapter
v0.1.1
Published
Airdraft base database adapter — abstract class, shared types, and contract test suite
Downloads
346
Readme
@airdraft/db-adapter
Base package for Airdraft database adapters. Provides the abstract BaseDatabaseAdapter class, shared TypeScript types, and a portable contract test suite that all driver packages run against.
You never install this package directly in a user project. It is a peer dependency of each driver package (@airdraft/db-adapter-sqlite, @airdraft/db-adapter-postgres, @airdraft/db-adapter-mongodb).
Architecture
Airdraft stores content as typed documents (entries). Each entry lives at a path <collection>/<slug>, carries an opaque sha (SHA-256 of the serialized content), and is tied to a projectId for multi-tenancy.
BaseDatabaseAdapter implements the full StorageAdapter interface expected by the CMS engine, so the engine, plugins, and client SDKs see no difference between a file adapter and a database adapter.
The engine detects a database adapter via instanceof BaseDatabaseAdapter and routes bulk list operations through the single-query queryEntries() method rather than the N+1 list() + read() loop used for file adapters.
Abstract methods (implement in your driver)
| Method | Description |
|---|---|
| migrate(): Promise<void> | Creates airdraft_entries and (when history: true) airdraft_entry_history. Idempotent — safe to call on every deploy. |
| read(path): Promise<FileResult \| null> | Reads a single entry by path. |
| write(path, content, options): Promise<void> | Creates or updates an entry. Must enforce SHA-based optimistic concurrency and throw ConflictError on mismatch. |
| delete(path, options): Promise<void> | Deletes an entry. |
| queryEntries(collection, options): Promise<QueryEntriesResult> | Bulk query: filters, sorts, and paginates in a single DB call. Called by the engine fast-path. |
| atomicUpdate(path, ops, sha): Promise<void> | Applies AtomicOp field operations (increment / set / push / pull) atomically. |
| rollback(path, sha): Promise<void> | Restore a previous revision from the history table. Only when history: true. |
| close(): Promise<void> | Release connection pool / file handles. |
Constructor options (DbAdapterOptions)
| Option | Type | Default | Description |
|---|---|---|---|
| projectId | string | 'default' | Isolates all reads/writes within a shared DB instance. Set to the Airdraft project slug for multi-tenant deployments. |
| history | boolean | false | When true, mirrors every write to airdraft_entry_history. Enables rollback(). |
| cacheTtlMs | number | 0 | Per-entry read cache TTL in milliseconds. 0 disables the cache. |
| cacheMaxSize | number | 500 | Maximum entries in the LRU read cache. |
Shared types
import type { DbAdapterOptions, QueryEntriesResult, AtomicOp } from '@airdraft/db-adapter'QueryEntriesResult
interface QueryEntriesResult {
entries: Array<{ path: string; content: string; sha: string }>
total: number // total matching rows before pagination — used for meta.total
}AtomicOp
type AtomicOp =
| { op: 'increment'; path: string; by: number }
| { op: 'set'; path: string; value: unknown }
| { op: 'push'; path: string; value: unknown }
| { op: 'pull'; path: string; value: unknown }path uses dot-notation — e.g. 'stats.viewCount' for a nested field.
Contract test suite
The @airdraft/db-adapter/testing export provides a shared Vitest test suite that all driver packages run against to verify full contract compliance.
// src/__tests__/contract.test.ts (inside a driver package)
import { describe } from 'vitest'
import { runStorageAdapterContract } from '@airdraft/db-adapter/testing'
import { SQLiteAdapter } from '../SQLiteAdapter.js'
describe('SQLiteAdapter contract', () => {
runStorageAdapterContract(() => new SQLiteAdapter({ filename: ':memory:', history: true }))
})The suite covers: migrate, write/read, ConflictError on SHA mismatch, delete, list, queryEntries (with filters, sort, pagination), atomicUpdate, history writes, and rollback.
Writing a custom adapter
Extend BaseDatabaseAdapter and implement all abstract methods:
import { BaseDatabaseAdapter } from '@airdraft/db-adapter'
import type { FileResult, WriteOptions, DeleteOptions, ListOptions } from '@airdraft/core'
import type { QueryEntriesResult, AtomicOp } from '@airdraft/db-adapter'
export class MyAdapter extends BaseDatabaseAdapter {
async migrate() { /* CREATE TABLE IF NOT EXISTS … */ }
async read(path: string): Promise<FileResult | null> { /* … */ }
async write(path: string, content: string, options: WriteOptions): Promise<void> { /* … */ }
async delete(path: string, options: DeleteOptions): Promise<void> { /* … */ }
async queryEntries(collection: string, options: ListOptions): Promise<QueryEntriesResult> { /* … */ }
async atomicUpdate(path: string, ops: AtomicOp[], sha: string): Promise<void> { /* … */ }
async rollback(path: string, sha: string): Promise<void> { /* … */ }
async close(): Promise<void> { /* … */ }
}Run the shared contract suite in your tests to confirm your implementation is spec-compliant.
License
MIT
