refactor: implement unified binary architecture

🏗️ **Major Architecture Refactoring: Separate CLI + Server → Unified Binary**

**Key Changes:**
 **Unified Binary**: Single 'insertr' binary with subcommands (enhance, serve)
 **Preserved Database Architecture**: Maintained sophisticated sqlc multi-DB setup
 **Smart Configuration**: Viper + YAML config with CLI flag precedence
 **Updated Build System**: Unified justfile, Air, and npm scripts

**Command Structure:**
- `insertr enhance [input-dir]` - Build-time content injection
- `insertr serve` - HTTP API server (dev + production modes)
- `insertr --config insertr.yaml` - YAML configuration support

**Architecture Benefits:**
- **Shared Database Layer**: Single source of truth for content models
- **Flexible Workflows**: Local DB for dev, remote API for production
- **Simple Deployment**: One binary for all use cases
- **Better UX**: Consistent configuration across build and runtime

**Preserved Features:**
- Multi-database support (SQLite + PostgreSQL)
- sqlc code generation and type safety
- Version control system with rollback
- Professional API endpoints
- Content enhancement pipeline

**Development Workflow:**
- `just dev` - Full-stack development (API server + demo site)
- `just serve` - API server only
- `just enhance` - Build-time content injection
- `air` - Hot reload unified binary

**Migration:** Consolidated insertr-cli/ and insertr-server/ → unified root structure
This commit is contained in:
2025-09-09 00:39:35 +02:00
parent 4dc479ba9e
commit e28000fd33
43 changed files with 4339 additions and 75 deletions

42
db/postgresql/schema.sql Normal file
View File

@@ -0,0 +1,42 @@
-- PostgreSQL-specific schema with BIGINT UNIX timestamps
-- Main content table (current versions only)
CREATE TABLE content (
id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('text', 'markdown', 'link')),
created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
updated_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
PRIMARY KEY (id, site_id)
);
-- Version history table for rollback functionality
CREATE TABLE content_versions (
version_id SERIAL PRIMARY KEY,
content_id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
type TEXT NOT NULL,
created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
created_by TEXT DEFAULT 'system' NOT NULL
);
-- Indexes for performance
CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id);
CREATE INDEX IF NOT EXISTS idx_content_updated_at ON content(updated_at);
CREATE INDEX IF NOT EXISTS idx_content_versions_lookup ON content_versions(content_id, site_id, created_at DESC);
-- Function and trigger to automatically update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = EXTRACT(EPOCH FROM NOW());
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_content_updated_at
BEFORE UPDATE ON content
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

47
db/postgresql/setup.sql Normal file
View File

@@ -0,0 +1,47 @@
-- name: InitializeSchema :exec
CREATE TABLE IF NOT EXISTS content (
id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('text', 'markdown', 'link')),
created_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
updated_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
PRIMARY KEY (id, site_id)
);
-- name: InitializeVersionsTable :exec
CREATE TABLE IF NOT EXISTS content_versions (
version_id SERIAL PRIMARY KEY,
content_id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
type TEXT NOT NULL,
created_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
created_by TEXT DEFAULT 'system' NOT NULL
);
-- name: CreateContentSiteIndex :exec
CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id);
-- name: CreateContentUpdatedAtIndex :exec
CREATE INDEX IF NOT EXISTS idx_content_updated_at ON content(updated_at);
-- name: CreateVersionsLookupIndex :exec
CREATE INDEX IF NOT EXISTS idx_content_versions_lookup ON content_versions(content_id, site_id, created_at DESC);
-- name: CreateUpdateFunction :exec
CREATE OR REPLACE FUNCTION update_content_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = EXTRACT(EPOCH FROM NOW());
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- name: CreateUpdateTrigger :exec
DROP TRIGGER IF EXISTS update_content_updated_at ON content;
CREATE TRIGGER update_content_updated_at
BEFORE UPDATE ON content
FOR EACH ROW
EXECUTE FUNCTION update_content_timestamp();

30
db/queries/content.sql Normal file
View File

@@ -0,0 +1,30 @@
-- name: GetContent :one
SELECT id, site_id, value, type, created_at, updated_at, last_edited_by
FROM content
WHERE id = sqlc.arg(id) AND site_id = sqlc.arg(site_id);
-- name: GetAllContent :many
SELECT id, site_id, value, type, created_at, updated_at, last_edited_by
FROM content
WHERE site_id = sqlc.arg(site_id)
ORDER BY updated_at DESC;
-- name: GetBulkContent :many
SELECT id, site_id, value, type, created_at, updated_at, last_edited_by
FROM content
WHERE site_id = sqlc.arg(site_id) AND id IN (sqlc.slice('ids'));
-- name: CreateContent :one
INSERT INTO content (id, site_id, value, type, last_edited_by)
VALUES (sqlc.arg(id), sqlc.arg(site_id), sqlc.arg(value), sqlc.arg(type), sqlc.arg(last_edited_by))
RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by;
-- name: UpdateContent :one
UPDATE content
SET value = sqlc.arg(value), type = sqlc.arg(type), last_edited_by = sqlc.arg(last_edited_by)
WHERE id = sqlc.arg(id) AND site_id = sqlc.arg(site_id)
RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by;
-- name: DeleteContent :exec
DELETE FROM content
WHERE id = sqlc.arg(id) AND site_id = sqlc.arg(site_id);

29
db/queries/versions.sql Normal file
View File

@@ -0,0 +1,29 @@
-- name: CreateContentVersion :exec
INSERT INTO content_versions (content_id, site_id, value, type, created_by)
VALUES (sqlc.arg(content_id), sqlc.arg(site_id), sqlc.arg(value), sqlc.arg(type), sqlc.arg(created_by));
-- name: GetContentVersionHistory :many
SELECT version_id, content_id, site_id, value, type, created_at, created_by
FROM content_versions
WHERE content_id = sqlc.arg(content_id) AND site_id = sqlc.arg(site_id)
ORDER BY created_at DESC
LIMIT sqlc.arg(limit_count);
-- name: GetContentVersion :one
SELECT version_id, content_id, site_id, value, type, created_at, created_by
FROM content_versions
WHERE version_id = sqlc.arg(version_id);
-- name: GetAllVersionsForSite :many
SELECT
cv.version_id, cv.content_id, cv.site_id, cv.value, cv.type, cv.created_at, cv.created_by,
c.value as current_value
FROM content_versions cv
LEFT JOIN content c ON cv.content_id = c.id AND cv.site_id = c.site_id
WHERE cv.site_id = sqlc.arg(site_id)
ORDER BY cv.created_at DESC
LIMIT sqlc.arg(limit_count);
-- name: DeleteOldVersions :exec
DELETE FROM content_versions
WHERE created_at < sqlc.arg(created_before) AND site_id = sqlc.arg(site_id);

36
db/sqlite/schema.sql Normal file
View File

@@ -0,0 +1,36 @@
-- SQLite-specific schema with INTEGER timestamps
-- Main content table (current versions only)
CREATE TABLE content (
id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('text', 'markdown', 'link')),
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
updated_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
PRIMARY KEY (id, site_id)
);
-- Version history table for rollback functionality
CREATE TABLE content_versions (
version_id INTEGER PRIMARY KEY AUTOINCREMENT,
content_id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
type TEXT NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
created_by TEXT DEFAULT 'system' NOT NULL
);
-- Indexes for performance
CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id);
CREATE INDEX IF NOT EXISTS idx_content_updated_at ON content(updated_at);
CREATE INDEX IF NOT EXISTS idx_content_versions_lookup ON content_versions(content_id, site_id, created_at DESC);
-- Trigger to automatically update updated_at timestamp
CREATE TRIGGER IF NOT EXISTS update_content_updated_at
AFTER UPDATE ON content
FOR EACH ROW
BEGIN
UPDATE content SET updated_at = strftime('%s', 'now') WHERE id = NEW.id AND site_id = NEW.site_id;
END;

39
db/sqlite/setup.sql Normal file
View File

@@ -0,0 +1,39 @@
-- name: InitializeSchema :exec
CREATE TABLE IF NOT EXISTS content (
id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('text', 'markdown', 'link')),
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
updated_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
PRIMARY KEY (id, site_id)
);
-- name: InitializeVersionsTable :exec
CREATE TABLE IF NOT EXISTS content_versions (
version_id INTEGER PRIMARY KEY AUTOINCREMENT,
content_id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
type TEXT NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
created_by TEXT DEFAULT 'system' NOT NULL
);
-- name: CreateContentSiteIndex :exec
CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id);
-- name: CreateContentUpdatedAtIndex :exec
CREATE INDEX IF NOT EXISTS idx_content_updated_at ON content(updated_at);
-- name: CreateVersionsLookupIndex :exec
CREATE INDEX IF NOT EXISTS idx_content_versions_lookup ON content_versions(content_id, site_id, created_at DESC);
-- name: CreateUpdateTrigger :exec
CREATE TRIGGER IF NOT EXISTS update_content_updated_at
AFTER UPDATE ON content
FOR EACH ROW
BEGIN
UPDATE content SET updated_at = strftime('%s', 'now') WHERE id = NEW.id AND site_id = NEW.site_id;
END;