update databasing system with minimum API, schema parsing and FreeCAD

integration
This commit is contained in:
Forbes
2026-01-24 15:03:17 -06:00
parent eafdc30f32
commit c327baf36f
51 changed files with 11635 additions and 0 deletions

313
cmd/silo/main.go Normal file
View File

@@ -0,0 +1,313 @@
// Command silo provides CLI access to the Silo item database.
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"github.com/kindredsystems/silo/internal/config"
"github.com/kindredsystems/silo/internal/db"
"github.com/kindredsystems/silo/internal/partnum"
"github.com/kindredsystems/silo/internal/schema"
)
func main() {
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
}
ctx := context.Background()
cmd := os.Args[1]
switch cmd {
case "help", "-h", "--help":
printUsage()
case "register":
cmdRegister(ctx)
case "list":
cmdList(ctx)
case "show":
cmdShow(ctx)
case "revisions":
cmdRevisions(ctx)
case "schemas":
cmdSchemas(ctx)
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", cmd)
printUsage()
os.Exit(1)
}
}
func printUsage() {
fmt.Println(`Silo - Item database CLI
Usage: silo <command> [options]
Commands:
register Generate a new part number and create item
list List items
show Show item details
revisions Show item revision history
schemas List available schemas
Examples:
silo register --schema kindred-rd --project PROTO --type AS
silo list --type assembly
silo show PROTO-AS-0001
silo revisions PROTO-AS-0001`)
}
func loadConfig() *config.Config {
cfg, err := config.Load("config.yaml")
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
os.Exit(1)
}
return cfg
}
func connectDB(ctx context.Context, cfg *config.Config) *db.DB {
database, err := db.Connect(ctx, db.Config{
Host: cfg.Database.Host,
Port: cfg.Database.Port,
Name: cfg.Database.Name,
User: cfg.Database.User,
Password: cfg.Database.Password,
SSLMode: cfg.Database.SSLMode,
MaxConnections: cfg.Database.MaxConnections,
})
if err != nil {
fmt.Fprintf(os.Stderr, "Error connecting to database: %v\n", err)
os.Exit(1)
}
return database
}
func cmdRegister(ctx context.Context) {
// Parse flags
var schemaName, project, partType, description string
args := os.Args[2:]
for i := 0; i < len(args); i++ {
switch args[i] {
case "--schema", "-s":
i++
schemaName = args[i]
case "--project", "-p":
i++
project = args[i]
case "--type", "-t":
i++
partType = args[i]
case "--description", "-d":
i++
description = args[i]
}
}
if schemaName == "" || project == "" || partType == "" {
fmt.Fprintln(os.Stderr, "Usage: silo register --schema NAME --project CODE --type TYPE [--description DESC]")
os.Exit(1)
}
cfg := loadConfig()
database := connectDB(ctx, cfg)
defer database.Close()
// Load schemas
schemas, err := schema.LoadAll(cfg.Schemas.Directory)
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading schemas: %v\n", err)
os.Exit(1)
}
// Create generator with DB-backed sequence store
seqStore := &dbSequenceStore{db: database, schemas: schemas}
gen := partnum.NewGenerator(schemas, seqStore)
// Generate part number
pn, err := gen.Generate(ctx, partnum.Input{
SchemaName: schemaName,
Values: map[string]string{
"project": project,
"part_type": partType,
},
})
if err != nil {
fmt.Fprintf(os.Stderr, "Error generating part number: %v\n", err)
os.Exit(1)
}
// Create item in database
repo := db.NewItemRepository(database)
schemaRecord := schemas[schemaName]
schemaID := getSchemaID(ctx, database, schemaRecord.Name)
item := &db.Item{
PartNumber: pn,
SchemaID: schemaID,
ItemType: mapPartType(partType),
Description: description,
}
if err := repo.Create(ctx, item, map[string]any{
"project": project,
"part_type": partType,
}); err != nil {
fmt.Fprintf(os.Stderr, "Error creating item: %v\n", err)
os.Exit(1)
}
fmt.Println(pn)
}
func cmdList(ctx context.Context) {
cfg := loadConfig()
database := connectDB(ctx, cfg)
defer database.Close()
var itemType, search string
args := os.Args[2:]
for i := 0; i < len(args); i++ {
switch args[i] {
case "--type", "-t":
i++
itemType = args[i]
case "--search", "-s":
i++
search = args[i]
}
}
repo := db.NewItemRepository(database)
items, err := repo.List(ctx, db.ListOptions{
ItemType: itemType,
Search: search,
Limit: 100,
})
if err != nil {
fmt.Fprintf(os.Stderr, "Error listing items: %v\n", err)
os.Exit(1)
}
for _, item := range items {
fmt.Printf("%s\t%s\t%s\n", item.PartNumber, item.ItemType, item.Description)
}
}
func cmdShow(ctx context.Context) {
if len(os.Args) < 3 {
fmt.Fprintln(os.Stderr, "Usage: silo show PART-NUMBER")
os.Exit(1)
}
partNumber := os.Args[2]
cfg := loadConfig()
database := connectDB(ctx, cfg)
defer database.Close()
repo := db.NewItemRepository(database)
item, err := repo.GetByPartNumber(ctx, partNumber)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if item == nil {
fmt.Fprintf(os.Stderr, "Item not found: %s\n", partNumber)
os.Exit(1)
}
out, _ := json.MarshalIndent(item, "", " ")
fmt.Println(string(out))
}
func cmdRevisions(ctx context.Context) {
if len(os.Args) < 3 {
fmt.Fprintln(os.Stderr, "Usage: silo revisions PART-NUMBER")
os.Exit(1)
}
partNumber := os.Args[2]
cfg := loadConfig()
database := connectDB(ctx, cfg)
defer database.Close()
repo := db.NewItemRepository(database)
item, err := repo.GetByPartNumber(ctx, partNumber)
if err != nil || item == nil {
fmt.Fprintf(os.Stderr, "Item not found: %s\n", partNumber)
os.Exit(1)
}
revisions, err := repo.GetRevisions(ctx, item.ID)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
for _, rev := range revisions {
comment := ""
if rev.Comment != nil {
comment = *rev.Comment
}
fmt.Printf("Rev %d\t%s\t%s\n", rev.RevisionNumber, rev.CreatedAt.Format("2006-01-02 15:04"), comment)
}
}
func cmdSchemas(ctx context.Context) {
cfg := loadConfig()
schemas, err := schema.LoadAll(cfg.Schemas.Directory)
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading schemas: %v\n", err)
os.Exit(1)
}
for name, s := range schemas {
fmt.Printf("%s\tv%d\t%s\n", name, s.Version, s.Description)
}
}
// dbSequenceStore implements partnum.SequenceStore using the database.
type dbSequenceStore struct {
db *db.DB
schemas map[string]*schema.Schema
}
func (s *dbSequenceStore) NextValue(ctx context.Context, schemaName string, scope string) (int, error) {
schemaID := getSchemaID(ctx, s.db, schemaName)
if schemaID == nil {
return 0, fmt.Errorf("schema not found: %s", schemaName)
}
return s.db.NextSequenceValue(ctx, *schemaID, scope)
}
func getSchemaID(ctx context.Context, database *db.DB, name string) *string {
var id string
err := database.Pool().QueryRow(ctx,
"SELECT id FROM schemas WHERE name = $1", name,
).Scan(&id)
if err != nil {
return nil
}
return &id
}
func mapPartType(code string) string {
types := map[string]string{
"AS": "assembly",
"PT": "part",
"DW": "drawing",
"DC": "document",
"TB": "tooling",
"PC": "purchased",
"EL": "electrical",
"SW": "software",
}
if t, ok := types[code]; ok {
return t
}
return code
}

126
cmd/silod/main.go Normal file
View File

@@ -0,0 +1,126 @@
// Command silod is the Silo HTTP API server.
package main
import (
"context"
"flag"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/kindredsystems/silo/internal/api"
"github.com/kindredsystems/silo/internal/config"
"github.com/kindredsystems/silo/internal/db"
"github.com/kindredsystems/silo/internal/schema"
"github.com/kindredsystems/silo/internal/storage"
"github.com/rs/zerolog"
)
func main() {
// Parse flags
configPath := flag.String("config", "config.yaml", "Path to configuration file")
flag.Parse()
// Setup logger
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
// Load configuration
cfg, err := config.Load(*configPath)
if err != nil {
logger.Fatal().Err(err).Msg("failed to load configuration")
}
logger.Info().
Str("host", cfg.Server.Host).
Int("port", cfg.Server.Port).
Str("database", cfg.Database.Host).
Str("storage", cfg.Storage.Endpoint).
Msg("starting silo server")
// Connect to database
ctx := context.Background()
database, err := db.Connect(ctx, db.Config{
Host: cfg.Database.Host,
Port: cfg.Database.Port,
Name: cfg.Database.Name,
User: cfg.Database.User,
Password: cfg.Database.Password,
SSLMode: cfg.Database.SSLMode,
MaxConnections: cfg.Database.MaxConnections,
})
if err != nil {
logger.Fatal().Err(err).Msg("failed to connect to database")
}
defer database.Close()
logger.Info().Msg("connected to database")
// Connect to storage (optional - may be externally managed)
var store *storage.Storage
if cfg.Storage.Endpoint != "" {
store, err = storage.Connect(ctx, storage.Config{
Endpoint: cfg.Storage.Endpoint,
AccessKey: cfg.Storage.AccessKey,
SecretKey: cfg.Storage.SecretKey,
Bucket: cfg.Storage.Bucket,
UseSSL: cfg.Storage.UseSSL,
Region: cfg.Storage.Region,
})
if err != nil {
logger.Warn().Err(err).Msg("failed to connect to storage - file operations disabled")
store = nil
} else {
logger.Info().Msg("connected to storage")
}
} else {
logger.Info().Msg("storage not configured - file operations disabled")
}
// Load schemas
schemas, err := schema.LoadAll(cfg.Schemas.Directory)
if err != nil {
logger.Fatal().Err(err).Str("directory", cfg.Schemas.Directory).Msg("failed to load schemas")
}
logger.Info().Int("count", len(schemas)).Msg("loaded schemas")
// Create API server
server := api.NewServer(logger, database, schemas, cfg.Schemas.Directory, store)
router := api.NewRouter(server, logger)
// Create HTTP server
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
httpServer := &http.Server{
Addr: addr,
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Start server in goroutine
go func() {
logger.Info().Str("addr", addr).Msg("listening")
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatal().Err(err).Msg("server error")
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Info().Msg("shutting down server")
// Graceful shutdown with timeout
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := httpServer.Shutdown(shutdownCtx); err != nil {
logger.Fatal().Err(err).Msg("server forced to shutdown")
}
logger.Info().Msg("server stopped")
}