314 lines
6.9 KiB
Go
314 lines
6.9 KiB
Go
// 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
|
|
}
|