- 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.
337 lines
9.4 KiB
Go
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
|
|
}
|