Files
relspecgo/pkg/readers/drawdb/reader.go
Hein 5d3c86119e
Some checks failed
CI / Test (1.24) (push) Successful in -27m28s
CI / Test (1.25) (push) Successful in -27m30s
CI / Build (push) Failing after -28m36s
Integration Tests / Integration Tests (push) Failing after -28m8s
CI / Lint (push) Successful in -27m54s
feat(domains): add domain support for DrawDB integration
- Introduce Domain and DomainTable models for logical grouping of tables.
- Implement export and import functionality for domains in DrawDB format.
- Update template execution modes to include domain processing.
- Enhance documentation for domain features and usage.
2026-01-04 15:49:47 +02:00

337 lines
9.4 KiB
Go

package drawdb
import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"git.warky.dev/wdevs/relspecgo/pkg/models"
"git.warky.dev/wdevs/relspecgo/pkg/readers"
"git.warky.dev/wdevs/relspecgo/pkg/writers/drawdb"
)
// Reader implements the readers.Reader interface for DrawDB JSON format
type Reader struct {
options *readers.ReaderOptions
}
// NewReader creates a new DrawDB reader with the given options
func NewReader(options *readers.ReaderOptions) *Reader {
return &Reader{
options: options,
}
}
// ReadDatabase reads and parses DrawDB JSON input, returning a Database model
func (r *Reader) ReadDatabase() (*models.Database, error) {
if r.options.FilePath == "" {
return nil, fmt.Errorf("file path is required for DrawDB reader")
}
data, err := os.ReadFile(r.options.FilePath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
var drawSchema drawdb.DrawDBSchema
if err := json.Unmarshal(data, &drawSchema); err != nil {
return nil, fmt.Errorf("failed to parse DrawDB JSON: %w", err)
}
return r.convertToDatabase(&drawSchema)
}
// ReadSchema reads and parses DrawDB JSON input, returning a Schema model
func (r *Reader) ReadSchema() (*models.Schema, error) {
if r.options.FilePath == "" {
return nil, fmt.Errorf("file path is required for DrawDB reader")
}
data, err := os.ReadFile(r.options.FilePath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
var drawSchema drawdb.DrawDBSchema
if err := json.Unmarshal(data, &drawSchema); err != nil {
return nil, fmt.Errorf("failed to parse DrawDB JSON: %w", err)
}
// Determine schema name from the first table, or use "public" as default
schemaName := "public"
if len(drawSchema.Tables) > 0 && drawSchema.Tables[0].Schema != "" {
schemaName = drawSchema.Tables[0].Schema
}
return r.convertToSchema(&drawSchema, schemaName)
}
// ReadTable reads and parses DrawDB JSON input, returning a Table model
func (r *Reader) ReadTable() (*models.Table, error) {
if r.options.FilePath == "" {
return nil, fmt.Errorf("file path is required for DrawDB reader")
}
data, err := os.ReadFile(r.options.FilePath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
var drawSchema drawdb.DrawDBSchema
if err := json.Unmarshal(data, &drawSchema); err != nil {
return nil, fmt.Errorf("failed to parse DrawDB JSON: %w", err)
}
if len(drawSchema.Tables) == 0 {
return nil, fmt.Errorf("no tables found in DrawDB JSON")
}
// Return the first table
return r.convertToTable(drawSchema.Tables[0], &drawSchema)
}
// convertToDatabase converts a DrawDB schema to a Database model
func (r *Reader) convertToDatabase(drawSchema *drawdb.DrawDBSchema) (*models.Database, error) {
db := models.InitDatabase("database")
if r.options.Metadata != nil {
if name, ok := r.options.Metadata["name"].(string); ok {
db.Name = name
}
}
// Extract database info from notes
for _, note := range drawSchema.Notes {
if strings.HasPrefix(note.Content, "Database:") {
parts := strings.SplitN(note.Content, "\n\n", 2)
if len(parts) == 2 {
db.Description = parts[1]
}
}
}
// Group tables by schema
schemaMap := make(map[string]*models.Schema)
for _, drawTable := range drawSchema.Tables {
schemaName := drawTable.Schema
if schemaName == "" {
schemaName = "public"
}
schema, exists := schemaMap[schemaName]
if !exists {
schema = models.InitSchema(schemaName)
schemaMap[schemaName] = schema
}
table, err := r.convertToTable(drawTable, drawSchema)
if err != nil {
return nil, fmt.Errorf("failed to convert table %s: %w", drawTable.Name, err)
}
schema.Tables = append(schema.Tables, table)
}
// Add schemas to database
for _, schema := range schemaMap {
db.Schemas = append(db.Schemas, schema)
}
// Convert DrawDB subject areas to domains
for _, area := range drawSchema.SubjectAreas {
domain := models.InitDomain(area.Name)
// Find all tables that visually belong to this area
// A table belongs to an area if its position is within the area bounds
for _, drawTable := range drawSchema.Tables {
if drawTable.X >= area.X && drawTable.X <= (area.X+area.Width) &&
drawTable.Y >= area.Y && drawTable.Y <= (area.Y+area.Height) {
schemaName := drawTable.Schema
if schemaName == "" {
schemaName = "public"
}
domainTable := models.InitDomainTable(drawTable.Name, schemaName)
domain.Tables = append(domain.Tables, domainTable)
}
}
// Only add domain if it has tables
if len(domain.Tables) > 0 {
db.Domains = append(db.Domains, domain)
}
}
return db, nil
}
// convertToSchema converts DrawDB tables to a Schema model
func (r *Reader) convertToSchema(drawSchema *drawdb.DrawDBSchema, schemaName string) (*models.Schema, error) {
schema := models.InitSchema(schemaName)
for _, drawTable := range drawSchema.Tables {
// Filter by schema if specified in the table
if drawTable.Schema != "" && drawTable.Schema != schemaName {
continue
}
table, err := r.convertToTable(drawTable, drawSchema)
if err != nil {
return nil, fmt.Errorf("failed to convert table %s: %w", drawTable.Name, err)
}
schema.Tables = append(schema.Tables, table)
}
return schema, nil
}
// convertToTable converts a DrawDB table to a Table model
func (r *Reader) convertToTable(drawTable *drawdb.DrawDBTable, drawSchema *drawdb.DrawDBSchema) (*models.Table, error) {
schemaName := drawTable.Schema
if schemaName == "" {
schemaName = "public"
}
table := models.InitTable(drawTable.Name, schemaName)
table.Description = drawTable.Comment
// Convert fields to columns
for _, field := range drawTable.Fields {
column := r.convertToColumn(field, drawTable.Name, schemaName)
table.Columns[column.Name] = column
}
// Convert indexes
for _, index := range drawTable.Indexes {
idx := r.convertToIndex(index, drawTable, schemaName)
table.Indexes[idx.Name] = idx
}
// Find and convert relationships/constraints for this table
for _, rel := range drawSchema.Relationships {
if rel.StartTableID == drawTable.ID {
constraint := r.convertToConstraint(rel, drawSchema)
if constraint != nil {
table.Constraints[constraint.Name] = constraint
}
}
}
return table, nil
}
// convertToColumn converts a DrawDB field to a Column model
func (r *Reader) convertToColumn(field *drawdb.DrawDBField, tableName, schemaName string) *models.Column {
column := models.InitColumn(field.Name, tableName, schemaName)
// Parse type and dimensions
typeStr := field.Type
column.Type = typeStr
// Try to extract length/precision from type string like "varchar(255)" or "decimal(10,2)"
if strings.Contains(typeStr, "(") {
parts := strings.Split(typeStr, "(")
column.Type = parts[0]
if len(parts) > 1 {
dimensions := strings.TrimSuffix(parts[1], ")")
if strings.Contains(dimensions, ",") {
// Precision and scale (e.g., decimal(10,2))
dims := strings.Split(dimensions, ",")
if precision, err := strconv.Atoi(strings.TrimSpace(dims[0])); err == nil {
column.Precision = precision
}
if len(dims) > 1 {
if scale, err := strconv.Atoi(strings.TrimSpace(dims[1])); err == nil {
column.Scale = scale
}
}
} else {
// Just length (e.g., varchar(255))
if length, err := strconv.Atoi(dimensions); err == nil {
column.Length = length
}
}
}
}
column.IsPrimaryKey = field.Primary
column.NotNull = field.NotNull || field.Primary
column.AutoIncrement = field.Increment
column.Comment = field.Comment
if field.Default != "" {
column.Default = field.Default
}
return column
}
// convertToIndex converts a DrawDB index to an Index model
func (r *Reader) convertToIndex(drawIndex *drawdb.DrawDBIndex, drawTable *drawdb.DrawDBTable, schemaName string) *models.Index {
index := models.InitIndex(drawIndex.Name, drawTable.Name, schemaName)
index.Table = drawTable.Name
index.Schema = schemaName
index.Unique = drawIndex.Unique
// Convert field IDs to column names
for _, fieldID := range drawIndex.Fields {
if fieldID >= 0 && fieldID < len(drawTable.Fields) {
index.Columns = append(index.Columns, drawTable.Fields[fieldID].Name)
}
}
return index
}
// convertToConstraint converts a DrawDB relationship to a Constraint model
func (r *Reader) convertToConstraint(rel *drawdb.DrawDBRelationship, drawSchema *drawdb.DrawDBSchema) *models.Constraint {
// Find the start and end tables
var startTable, endTable *drawdb.DrawDBTable
for _, table := range drawSchema.Tables {
if table.ID == rel.StartTableID {
startTable = table
}
if table.ID == rel.EndTableID {
endTable = table
}
}
if startTable == nil || endTable == nil {
return nil
}
constraint := models.InitConstraint(rel.Name, models.ForeignKeyConstraint)
// Get the column names from field IDs
if rel.StartFieldID >= 0 && rel.StartFieldID < len(startTable.Fields) {
constraint.Columns = append(constraint.Columns, startTable.Fields[rel.StartFieldID].Name)
}
if rel.EndFieldID >= 0 && rel.EndFieldID < len(endTable.Fields) {
constraint.ReferencedColumns = append(constraint.ReferencedColumns, endTable.Fields[rel.EndFieldID].Name)
}
constraint.Table = startTable.Name
constraint.Schema = startTable.Schema
if constraint.Schema == "" {
constraint.Schema = "public"
}
constraint.ReferencedTable = endTable.Name
constraint.ReferencedSchema = endTable.Schema
if constraint.ReferencedSchema == "" {
constraint.ReferencedSchema = "public"
}
constraint.OnUpdate = rel.UpdateConstraint
constraint.OnDelete = rel.DeleteConstraint
return constraint
}