🏗️ **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
185 lines
5.3 KiB
Go
185 lines
5.3 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
|
|
_ "github.com/lib/pq"
|
|
_ "github.com/mattn/go-sqlite3"
|
|
|
|
"github.com/insertr/insertr/internal/db/postgresql"
|
|
"github.com/insertr/insertr/internal/db/sqlite"
|
|
)
|
|
|
|
// Database wraps the database connection and queries
|
|
type Database struct {
|
|
conn *sql.DB
|
|
dbType string
|
|
|
|
// Type-specific query interfaces
|
|
sqliteQueries *sqlite.Queries
|
|
postgresqlQueries *postgresql.Queries
|
|
}
|
|
|
|
// NewDatabase creates a new database connection
|
|
func NewDatabase(dbPath string) (*Database, error) {
|
|
var conn *sql.DB
|
|
var dbType string
|
|
var err error
|
|
|
|
// Determine database type from connection string
|
|
if strings.Contains(dbPath, "postgres://") || strings.Contains(dbPath, "postgresql://") {
|
|
dbType = "postgresql"
|
|
conn, err = sql.Open("postgres", dbPath)
|
|
} else {
|
|
dbType = "sqlite3"
|
|
conn, err = sql.Open("sqlite3", dbPath)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open database: %w", err)
|
|
}
|
|
|
|
// Test connection
|
|
if err := conn.Ping(); err != nil {
|
|
conn.Close()
|
|
return nil, fmt.Errorf("failed to ping database: %w", err)
|
|
}
|
|
|
|
// Initialize the appropriate queries
|
|
db := &Database{
|
|
conn: conn,
|
|
dbType: dbType,
|
|
}
|
|
|
|
switch dbType {
|
|
case "sqlite3":
|
|
// Initialize SQLite schema using generated functions
|
|
db.sqliteQueries = sqlite.New(conn)
|
|
if err := db.initializeSQLiteSchema(); err != nil {
|
|
conn.Close()
|
|
return nil, fmt.Errorf("failed to initialize SQLite schema: %w", err)
|
|
}
|
|
case "postgresql":
|
|
// Initialize PostgreSQL schema using generated functions
|
|
db.postgresqlQueries = postgresql.New(conn)
|
|
if err := db.initializePostgreSQLSchema(); err != nil {
|
|
conn.Close()
|
|
return nil, fmt.Errorf("failed to initialize PostgreSQL schema: %w", err)
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("unsupported database type: %s", dbType)
|
|
}
|
|
|
|
return db, nil
|
|
}
|
|
|
|
// Close closes the database connection
|
|
func (db *Database) Close() error {
|
|
return db.conn.Close()
|
|
}
|
|
|
|
// GetQueries returns the appropriate query interface
|
|
func (db *Database) GetSQLiteQueries() *sqlite.Queries {
|
|
return db.sqliteQueries
|
|
}
|
|
|
|
func (db *Database) GetPostgreSQLQueries() *postgresql.Queries {
|
|
return db.postgresqlQueries
|
|
}
|
|
|
|
// GetDBType returns the database type
|
|
func (db *Database) GetDBType() string {
|
|
return db.dbType
|
|
}
|
|
|
|
// initializeSQLiteSchema sets up the SQLite database schema
|
|
func (db *Database) initializeSQLiteSchema() error {
|
|
ctx := context.Background()
|
|
|
|
// Create tables
|
|
if err := db.sqliteQueries.InitializeSchema(ctx); err != nil {
|
|
return fmt.Errorf("failed to create content table: %w", err)
|
|
}
|
|
|
|
if err := db.sqliteQueries.InitializeVersionsTable(ctx); err != nil {
|
|
return fmt.Errorf("failed to create content_versions table: %w", err)
|
|
}
|
|
|
|
// Create indexes manually (sqlc doesn't generate CREATE INDEX functions for SQLite)
|
|
indexQueries := []string{
|
|
"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);",
|
|
}
|
|
|
|
for _, query := range indexQueries {
|
|
if _, err := db.conn.Exec(query); err != nil {
|
|
return fmt.Errorf("failed to create index: %w", err)
|
|
}
|
|
}
|
|
|
|
// Create update trigger manually (sqlc doesn't generate trigger creation functions)
|
|
triggerQuery := `
|
|
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;`
|
|
|
|
if _, err := db.conn.Exec(triggerQuery); err != nil {
|
|
return fmt.Errorf("failed to create update trigger: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// initializePostgreSQLSchema sets up the PostgreSQL database schema
|
|
func (db *Database) initializePostgreSQLSchema() error {
|
|
ctx := context.Background()
|
|
|
|
// Create tables using sqlc-generated functions
|
|
if err := db.postgresqlQueries.InitializeSchema(ctx); err != nil {
|
|
return fmt.Errorf("failed to create content table: %w", err)
|
|
}
|
|
|
|
if err := db.postgresqlQueries.InitializeVersionsTable(ctx); err != nil {
|
|
return fmt.Errorf("failed to create content_versions table: %w", err)
|
|
}
|
|
|
|
// Create indexes using sqlc-generated functions (PostgreSQL supports this)
|
|
if err := db.postgresqlQueries.CreateContentSiteIndex(ctx); err != nil {
|
|
return fmt.Errorf("failed to create content site index: %w", err)
|
|
}
|
|
|
|
if err := db.postgresqlQueries.CreateContentUpdatedAtIndex(ctx); err != nil {
|
|
return fmt.Errorf("failed to create content updated_at index: %w", err)
|
|
}
|
|
|
|
if err := db.postgresqlQueries.CreateVersionsLookupIndex(ctx); err != nil {
|
|
return fmt.Errorf("failed to create versions lookup index: %w", err)
|
|
}
|
|
|
|
// Create update function using sqlc-generated function
|
|
if err := db.postgresqlQueries.CreateUpdateFunction(ctx); err != nil {
|
|
return fmt.Errorf("failed to create update function: %w", err)
|
|
}
|
|
|
|
// Create trigger manually (sqlc doesn't generate trigger creation functions)
|
|
triggerQuery := `
|
|
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();`
|
|
|
|
if _, err := db.conn.Exec(triggerQuery); err != nil {
|
|
return fmt.Errorf("failed to create update trigger: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|