/** * Database Adapter Functions * * These run at config time (astro.config.mjs) and return serializable descriptors. * The actual dialect is created at runtime by loading the entrypoint. * * @example * ```ts * // astro.config.mjs * import emdash from "@premium-cms/emdash/astro"; * import { sqlite } from "@premium-cms/emdash/db"; * * export default defineConfig({ * integrations: [ * emdash({ * database: sqlite({ url: "file:./data.db" }), * }), * ], * }); * ``` */ /** * Dialect family identifier. * Used at runtime to select dialect-specific SQL fragments. */ export type DatabaseDialectType = "sqlite" | "postgres"; export type CollectionDeletionGuardInput = | { action: "fence"; collectionId: string; collectionSlug: string; leaseToken: string; forceDelete: boolean; } | { action: "drop"; collectionId: string; collectionSlug: string; leaseToken: string; }; export type CollectionDeletionGuardResult = | { outcome: "fenced" } | { outcome: "has_content" } | { outcome: "stale" } | { outcome: "dropped" }; export type ExecuteCollectionDeletionGuard = ( config: unknown, input: CollectionDeletionGuardInput, ) => Promise; const ENVIRONMENT_VARIABLE_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/; function migrationEnvironmentVariable( value: string | undefined, fallback: string, optionName: string, ): string { const name = value ?? fallback; if (!ENVIRONMENT_VARIABLE_PATTERN.test(name)) { throw new Error(`${optionName} must be a valid environment variable name.`); } return name; } /** * Database descriptor - serializable config for virtual modules */ export interface DatabaseDescriptor { entrypoint: string; config: unknown; type: DatabaseDialectType; /** Deployment migration capability with configuration safe for a build artifact. */ migrations?: { entrypoint: string; manifestConfig: unknown; }; /** * When true, the adapter's runtime entrypoint MUST export a named * `createRequestScopedDb` function matching the signature declared in * `virtual:emdash/dialect`. The virtual-module generator re-exports it * by name, so a missing export becomes a build-time bundler error. * * The function is called once per request and decides — based on its own * runtime config (e.g. whether the user opted into D1 sessions) — whether * to return a per-request Kysely or null. Use this for features like D1 * read-replica sessions, bookmark cookies, or any per-request DB handle. * * When false or absent, the generator emits a stub that returns null and * the middleware takes its default (singleton) path. */ supportsRequestScope?: boolean; /** * When true, request middleware resolves the last content-namespace * invalidation timestamp and passes it to `createRequestScopedDb`. * * Keep this unset unless request routing depends on that timestamp: reading * it may require an object-cache backend round trip. */ needsLastContentWriteAt?: boolean; /** * When true, the adapter's runtime entrypoint MUST export a named * `createCoalescingDialect` function. The runtime uses this fresh dialect * only for its cold-start read batch. * * When false or absent, the virtual module exports `undefined` without * inspecting an optional entrypoint export. */ supportsCoalescing?: boolean; /** The runtime entrypoint exports the deletion-specific atomic guard. */ supportsCollectionDeletionGuard?: boolean; } export interface SqliteConfig { /** * Database URL (e.g., "file:./data.db") */ url: string; } export interface LibsqlConfig { /** * Database URL (e.g., "file:./data.db" or "libsql://...") */ url: string; /** * Auth token for remote libSQL */ authToken?: string; migrationAuthTokenEnv?: string; } export interface SnapshotLiveConfig { /** * Backend origin to pull the content snapshot from * (e.g. "https://beta.saastemly.com") */ url: string; /** * API token with content:read + schema:read + `GET /snapshot` (the frontend * service account's). Falls back to process.env.EMDASH_API_TOKEN at runtime. */ token?: string; /** Ask the backend for draft content too (?drafts=true). */ includeDrafts?: boolean; /** Directory holding git-backed collection entries (default "content"). */ contentDir?: string; /** * Re-fetch the snapshot when it is older than this many ms * (default 2000; <= 0 disables refresh). */ refreshMs?: number; } /** * SQLite database adapter (better-sqlite3) * * For local development and Node.js deployments. * * @example * ```ts * database: sqlite({ url: "file:./data.db" }) * ``` */ export function sqlite(config: SqliteConfig): DatabaseDescriptor { return { entrypoint: "@premium-cms/emdash/db/sqlite", config, type: "sqlite", migrations: { entrypoint: "@premium-cms/emdash/db/sqlite-migrations", manifestConfig: { url: config.url }, }, }; } /** * Live-snapshot adapter — an in-memory SQLite database continuously refreshed * from a live backend's `/_emdash/api/snapshot`. For `astro dev` (and one-shot * builds) against a deployed instance: no local backend, no snapshot file — * the same data path the platform's builds and previews use, kept live. * * @example * ```ts * database: snapshotLive({ * url: "https://example.com", * token: process.env.EMDASH_API_TOKEN, * }) * ``` */ export function snapshotLive(config: SnapshotLiveConfig): DatabaseDescriptor { return { entrypoint: "@premium-cms/emdash/db/snapshot-live", config, type: "sqlite", }; } /** * libSQL database adapter (Turso) * * For Turso hosted databases or local libSQL. * * @example * ```ts * database: libsql({ * url: "libsql://my-db.turso.io", * authToken: process.env.TURSO_AUTH_TOKEN, * }) * ``` */ export function libsql(config: LibsqlConfig): DatabaseDescriptor { const { migrationAuthTokenEnv, ...runtimeConfig } = config; return { entrypoint: "@premium-cms/emdash/db/libsql", config: runtimeConfig, type: "sqlite", migrations: { entrypoint: "@premium-cms/emdash/db/libsql-migrations", manifestConfig: { url: config.url, authTokenEnv: migrationEnvironmentVariable( migrationAuthTokenEnv, "TURSO_AUTH_TOKEN", "migrationAuthTokenEnv", ), }, }, }; } /** * PostgreSQL connection configuration */ export interface PostgresConfig { connectionString?: string; host?: string; port?: number; database?: string; user?: string; password?: string; ssl?: boolean; pool?: { min?: number; max?: number }; migrationConnectionStringEnv?: string; } /** * PostgreSQL database adapter * * For PostgreSQL deployments with connection pooling. * * @example * ```ts * database: postgres({ connectionString: process.env.DATABASE_URL }) * ``` */ export function postgres(config: PostgresConfig): DatabaseDescriptor { const { migrationConnectionStringEnv, ...runtimeConfig } = config; return { entrypoint: "@premium-cms/emdash/db/postgres", config: runtimeConfig, type: "postgres", migrations: { entrypoint: "@premium-cms/emdash/db/postgres-migrations", manifestConfig: { connectionStringEnv: migrationEnvironmentVariable( migrationConnectionStringEnv, "DATABASE_URL", "migrationConnectionStringEnv", ), }, }, }; }