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

View File

@@ -0,0 +1,214 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: content.sql
package postgresql
import (
"context"
"strings"
)
const createContent = `-- name: CreateContent :one
INSERT INTO content (id, site_id, value, type, last_edited_by)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by
`
type CreateContentParams struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
LastEditedBy string `json:"last_edited_by"`
}
func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (Content, error) {
row := q.db.QueryRowContext(ctx, createContent,
arg.ID,
arg.SiteID,
arg.Value,
arg.Type,
arg.LastEditedBy,
)
var i Content
err := row.Scan(
&i.ID,
&i.SiteID,
&i.Value,
&i.Type,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
)
return i, err
}
const deleteContent = `-- name: DeleteContent :exec
DELETE FROM content
WHERE id = $1 AND site_id = $2
`
type DeleteContentParams struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
}
func (q *Queries) DeleteContent(ctx context.Context, arg DeleteContentParams) error {
_, err := q.db.ExecContext(ctx, deleteContent, arg.ID, arg.SiteID)
return err
}
const getAllContent = `-- name: GetAllContent :many
SELECT id, site_id, value, type, created_at, updated_at, last_edited_by
FROM content
WHERE site_id = $1
ORDER BY updated_at DESC
`
func (q *Queries) GetAllContent(ctx context.Context, siteID string) ([]Content, error) {
rows, err := q.db.QueryContext(ctx, getAllContent, siteID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Content
for rows.Next() {
var i Content
if err := rows.Scan(
&i.ID,
&i.SiteID,
&i.Value,
&i.Type,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getBulkContent = `-- name: GetBulkContent :many
SELECT id, site_id, value, type, created_at, updated_at, last_edited_by
FROM content
WHERE site_id = $1 AND id IN ($2)
`
type GetBulkContentParams struct {
SiteID string `json:"site_id"`
Ids []string `json:"ids"`
}
func (q *Queries) GetBulkContent(ctx context.Context, arg GetBulkContentParams) ([]Content, error) {
query := getBulkContent
var queryParams []interface{}
queryParams = append(queryParams, arg.SiteID)
if len(arg.Ids) > 0 {
for _, v := range arg.Ids {
queryParams = append(queryParams, v)
}
query = strings.Replace(query, "/*SLICE:ids*/?", strings.Repeat(",?", len(arg.Ids))[1:], 1)
} else {
query = strings.Replace(query, "/*SLICE:ids*/?", "NULL", 1)
}
rows, err := q.db.QueryContext(ctx, query, queryParams...)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Content
for rows.Next() {
var i Content
if err := rows.Scan(
&i.ID,
&i.SiteID,
&i.Value,
&i.Type,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getContent = `-- name: GetContent :one
SELECT id, site_id, value, type, created_at, updated_at, last_edited_by
FROM content
WHERE id = $1 AND site_id = $2
`
type GetContentParams struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
}
func (q *Queries) GetContent(ctx context.Context, arg GetContentParams) (Content, error) {
row := q.db.QueryRowContext(ctx, getContent, arg.ID, arg.SiteID)
var i Content
err := row.Scan(
&i.ID,
&i.SiteID,
&i.Value,
&i.Type,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
)
return i, err
}
const updateContent = `-- name: UpdateContent :one
UPDATE content
SET value = $1, type = $2, last_edited_by = $3
WHERE id = $4 AND site_id = $5
RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by
`
type UpdateContentParams struct {
Value string `json:"value"`
Type string `json:"type"`
LastEditedBy string `json:"last_edited_by"`
ID string `json:"id"`
SiteID string `json:"site_id"`
}
func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (Content, error) {
row := q.db.QueryRowContext(ctx, updateContent,
arg.Value,
arg.Type,
arg.LastEditedBy,
arg.ID,
arg.SiteID,
)
var i Content
err := row.Scan(
&i.ID,
&i.SiteID,
&i.Value,
&i.Type,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
)
return i, err
}

View File

@@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
package postgresql
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

View File

@@ -0,0 +1,25 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
package postgresql
type Content struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
LastEditedBy string `json:"last_edited_by"`
}
type ContentVersion struct {
VersionID int32 `json:"version_id"`
ContentID string `json:"content_id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
CreatedAt int64 `json:"created_at"`
CreatedBy string `json:"created_by"`
}

View File

@@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
package postgresql
import (
"context"
)
type Querier interface {
CreateContent(ctx context.Context, arg CreateContentParams) (Content, error)
CreateContentSiteIndex(ctx context.Context) error
CreateContentUpdatedAtIndex(ctx context.Context) error
CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error
CreateUpdateFunction(ctx context.Context) error
CreateVersionsLookupIndex(ctx context.Context) error
DeleteContent(ctx context.Context, arg DeleteContentParams) error
DeleteOldVersions(ctx context.Context, arg DeleteOldVersionsParams) error
GetAllContent(ctx context.Context, siteID string) ([]Content, error)
GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsForSiteParams) ([]GetAllVersionsForSiteRow, error)
GetBulkContent(ctx context.Context, arg GetBulkContentParams) ([]Content, error)
GetContent(ctx context.Context, arg GetContentParams) (Content, error)
GetContentVersion(ctx context.Context, versionID int32) (ContentVersion, error)
GetContentVersionHistory(ctx context.Context, arg GetContentVersionHistoryParams) ([]ContentVersion, error)
InitializeSchema(ctx context.Context) error
InitializeVersionsTable(ctx context.Context) error
UpdateContent(ctx context.Context, arg UpdateContentParams) (Content, error)
}
var _ Querier = (*Queries)(nil)

View File

@@ -0,0 +1,87 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: setup.sql
package postgresql
import (
"context"
)
const createContentSiteIndex = `-- name: CreateContentSiteIndex :exec
CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id)
`
func (q *Queries) CreateContentSiteIndex(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, createContentSiteIndex)
return err
}
const createContentUpdatedAtIndex = `-- name: CreateContentUpdatedAtIndex :exec
CREATE INDEX IF NOT EXISTS idx_content_updated_at ON content(updated_at)
`
func (q *Queries) CreateContentUpdatedAtIndex(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, createContentUpdatedAtIndex)
return err
}
const createUpdateFunction = `-- 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
`
func (q *Queries) CreateUpdateFunction(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, createUpdateFunction)
return err
}
const createVersionsLookupIndex = `-- name: CreateVersionsLookupIndex :exec
CREATE INDEX IF NOT EXISTS idx_content_versions_lookup ON content_versions(content_id, site_id, created_at DESC)
`
func (q *Queries) CreateVersionsLookupIndex(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, createVersionsLookupIndex)
return err
}
const initializeSchema = `-- 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)
)
`
func (q *Queries) InitializeSchema(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, initializeSchema)
return err
}
const initializeVersionsTable = `-- 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
)
`
func (q *Queries) InitializeVersionsTable(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, initializeVersionsTable)
return err
}

View File

@@ -0,0 +1,175 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: versions.sql
package postgresql
import (
"context"
"database/sql"
)
const createContentVersion = `-- name: CreateContentVersion :exec
INSERT INTO content_versions (content_id, site_id, value, type, created_by)
VALUES ($1, $2, $3, $4, $5)
`
type CreateContentVersionParams struct {
ContentID string `json:"content_id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
CreatedBy string `json:"created_by"`
}
func (q *Queries) CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error {
_, err := q.db.ExecContext(ctx, createContentVersion,
arg.ContentID,
arg.SiteID,
arg.Value,
arg.Type,
arg.CreatedBy,
)
return err
}
const deleteOldVersions = `-- name: DeleteOldVersions :exec
DELETE FROM content_versions
WHERE created_at < $1 AND site_id = $2
`
type DeleteOldVersionsParams struct {
CreatedBefore int64 `json:"created_before"`
SiteID string `json:"site_id"`
}
func (q *Queries) DeleteOldVersions(ctx context.Context, arg DeleteOldVersionsParams) error {
_, err := q.db.ExecContext(ctx, deleteOldVersions, arg.CreatedBefore, arg.SiteID)
return err
}
const getAllVersionsForSite = `-- 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 = $1
ORDER BY cv.created_at DESC
LIMIT $2
`
type GetAllVersionsForSiteParams struct {
SiteID string `json:"site_id"`
LimitCount int32 `json:"limit_count"`
}
type GetAllVersionsForSiteRow struct {
VersionID int32 `json:"version_id"`
ContentID string `json:"content_id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
CreatedAt int64 `json:"created_at"`
CreatedBy string `json:"created_by"`
CurrentValue sql.NullString `json:"current_value"`
}
func (q *Queries) GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsForSiteParams) ([]GetAllVersionsForSiteRow, error) {
rows, err := q.db.QueryContext(ctx, getAllVersionsForSite, arg.SiteID, arg.LimitCount)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAllVersionsForSiteRow
for rows.Next() {
var i GetAllVersionsForSiteRow
if err := rows.Scan(
&i.VersionID,
&i.ContentID,
&i.SiteID,
&i.Value,
&i.Type,
&i.CreatedAt,
&i.CreatedBy,
&i.CurrentValue,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getContentVersion = `-- name: GetContentVersion :one
SELECT version_id, content_id, site_id, value, type, created_at, created_by
FROM content_versions
WHERE version_id = $1
`
func (q *Queries) GetContentVersion(ctx context.Context, versionID int32) (ContentVersion, error) {
row := q.db.QueryRowContext(ctx, getContentVersion, versionID)
var i ContentVersion
err := row.Scan(
&i.VersionID,
&i.ContentID,
&i.SiteID,
&i.Value,
&i.Type,
&i.CreatedAt,
&i.CreatedBy,
)
return i, err
}
const getContentVersionHistory = `-- name: GetContentVersionHistory :many
SELECT version_id, content_id, site_id, value, type, created_at, created_by
FROM content_versions
WHERE content_id = $1 AND site_id = $2
ORDER BY created_at DESC
LIMIT $3
`
type GetContentVersionHistoryParams struct {
ContentID string `json:"content_id"`
SiteID string `json:"site_id"`
LimitCount int32 `json:"limit_count"`
}
func (q *Queries) GetContentVersionHistory(ctx context.Context, arg GetContentVersionHistoryParams) ([]ContentVersion, error) {
rows, err := q.db.QueryContext(ctx, getContentVersionHistory, arg.ContentID, arg.SiteID, arg.LimitCount)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ContentVersion
for rows.Next() {
var i ContentVersion
if err := rows.Scan(
&i.VersionID,
&i.ContentID,
&i.SiteID,
&i.Value,
&i.Type,
&i.CreatedAt,
&i.CreatedBy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}