Files
relspecgo/pkg/writers/drawdb/writer.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

396 lines
10 KiB
Go

package drawdb
import (
"encoding/json"
"fmt"
"os"
"git.warky.dev/wdevs/relspecgo/pkg/models"
"git.warky.dev/wdevs/relspecgo/pkg/writers"
)
// Writer implements the writers.Writer interface for DrawDB JSON format
type Writer struct {
options *writers.WriterOptions
}
// NewWriter creates a new DrawDB writer with the given options
func NewWriter(options *writers.WriterOptions) *Writer {
return &Writer{
options: options,
}
}
// WriteDatabase writes a Database model to DrawDB JSON format
func (w *Writer) WriteDatabase(db *models.Database) error {
schema := w.databaseToDrawDB(db)
return w.writeJSON(schema)
}
// WriteSchema writes a Schema model to DrawDB JSON format
func (w *Writer) WriteSchema(schema *models.Schema) error {
drawSchema := w.schemaToDrawDB(schema)
return w.writeJSON(drawSchema)
}
// WriteTable writes a Table model to DrawDB JSON format
func (w *Writer) WriteTable(table *models.Table) error {
drawSchema := w.tableToDrawDB(table)
return w.writeJSON(drawSchema)
}
// writeJSON marshals the data to JSON and writes to output
func (w *Writer) writeJSON(data interface{}) error {
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal to JSON: %w", err)
}
if w.options.OutputPath != "" {
return os.WriteFile(w.options.OutputPath, jsonData, 0644)
}
// If no output path, print to stdout
fmt.Println(string(jsonData))
return nil
}
// databaseToDrawDB converts a Database to DrawDB JSON format
func (w *Writer) databaseToDrawDB(d *models.Database) *DrawDBSchema {
schema := &DrawDBSchema{
Tables: make([]*DrawDBTable, 0),
Relationships: make([]*DrawDBRelationship, 0),
Notes: make([]*DrawDBNote, 0),
SubjectAreas: make([]*DrawDBArea, 0),
}
// Track IDs and mappings
tableID := 0
fieldID := 0
relationshipID := 0
noteID := 0
areaID := 0
// Map to track table name to ID
tableMap := make(map[string]int)
// Map to track field full path to ID
fieldMap := make(map[string]int)
// Position tables in a grid layout
gridX, gridY := 50, 50
colWidth, rowHeight := 300, 200
tablesPerRow := 4
tableIndex := 0
// Create subject areas for schemas
for schemaIdx, schemaModel := range d.Schemas {
if schemaModel.Description != "" || schemaModel.Comment != "" {
note := schemaModel.Description
if note != "" && schemaModel.Comment != "" {
note += "\n"
}
note += schemaModel.Comment
_ = note // TODO: Add note/description field to DrawDBArea when supported
area := &DrawDBArea{
ID: areaID,
Name: schemaModel.Name,
Color: getColorForIndex(schemaIdx),
X: gridX - 20,
Y: gridY - 20,
Width: colWidth*tablesPerRow + 100,
Height: rowHeight*((len(schemaModel.Tables)/tablesPerRow)+1) + 100,
}
schema.SubjectAreas = append(schema.SubjectAreas, area)
areaID++
}
// Process tables in schema
for _, table := range schemaModel.Tables {
drawTable, newFieldID := w.convertTableToDrawDB(table, schemaModel.Name, tableID, fieldID, tableIndex, tablesPerRow, gridX, gridY, colWidth, rowHeight, schemaIdx)
// Store table mapping
tableKey := fmt.Sprintf("%s.%s", schemaModel.Name, table.Name)
tableMap[tableKey] = tableID
// Store field mappings
for _, field := range drawTable.Fields {
fieldKey := fmt.Sprintf("%s.%s.%s", schemaModel.Name, table.Name, field.Name)
fieldMap[fieldKey] = field.ID
}
schema.Tables = append(schema.Tables, drawTable)
fieldID = newFieldID
tableID++
tableIndex++
}
}
// Create subject areas for domains
for domainIdx, domainModel := range d.Domains {
// Calculate bounds for all tables in this domain
minX, minY := 999999, 999999
maxX, maxY := 0, 0
domainTableCount := 0
for _, domainTable := range domainModel.Tables {
// Find the table in the schema to get its position
for _, t := range schema.Tables {
if t.Name == domainTable.TableName {
if t.X < minX {
minX = t.X
}
if t.Y < minY {
minY = t.Y
}
if t.X+colWidth > maxX {
maxX = t.X + colWidth
}
if t.Y+rowHeight > maxY {
maxY = t.Y + rowHeight
}
domainTableCount++
break
}
}
}
// Only create area if domain has tables in this schema
if domainTableCount > 0 {
area := &DrawDBArea{
ID: areaID,
Name: domainModel.Name,
Color: getColorForIndex(len(d.Schemas) + domainIdx), // Use different colors than schemas
X: minX - 20,
Y: minY - 20,
Width: maxX - minX + 40,
Height: maxY - minY + 40,
}
schema.SubjectAreas = append(schema.SubjectAreas, area)
areaID++
}
}
// Add relationships
for _, schemaModel := range d.Schemas {
for _, table := range schemaModel.Tables {
for _, constraint := range table.Constraints {
if constraint.Type == models.ForeignKeyConstraint && constraint.ReferencedTable != "" {
startTableKey := fmt.Sprintf("%s.%s", schemaModel.Name, table.Name)
endTableKey := fmt.Sprintf("%s.%s", constraint.ReferencedSchema, constraint.ReferencedTable)
startTableID, startExists := tableMap[startTableKey]
endTableID, endExists := tableMap[endTableKey]
if startExists && endExists && len(constraint.Columns) > 0 && len(constraint.ReferencedColumns) > 0 {
// Find relative field IDs within their tables
startFieldID := 0
endFieldID := 0
for _, t := range schema.Tables {
if t.ID == startTableID {
for idx, f := range t.Fields {
if f.Name == constraint.Columns[0] {
startFieldID = idx
break
}
}
}
if t.ID == endTableID {
for idx, f := range t.Fields {
if f.Name == constraint.ReferencedColumns[0] {
endFieldID = idx
break
}
}
}
}
relationship := &DrawDBRelationship{
ID: relationshipID,
Name: constraint.Name,
StartTableID: startTableID,
EndTableID: endTableID,
StartFieldID: startFieldID,
EndFieldID: endFieldID,
Cardinality: "Many to one",
UpdateConstraint: constraint.OnUpdate,
DeleteConstraint: constraint.OnDelete,
}
schema.Relationships = append(schema.Relationships, relationship)
relationshipID++
}
}
}
}
}
// Add database description as a note
if d.Description != "" || d.Comment != "" {
note := d.Description
if note != "" && d.Comment != "" {
note += "\n"
}
note += d.Comment
schema.Notes = append(schema.Notes, &DrawDBNote{
ID: noteID,
Content: fmt.Sprintf("Database: %s\n\n%s", d.Name, note),
Color: "#ffd93d",
X: 10,
Y: 10,
})
}
return schema
}
// schemaToDrawDB converts a Schema to DrawDB format
func (w *Writer) schemaToDrawDB(schema *models.Schema) *DrawDBSchema {
drawSchema := &DrawDBSchema{
Tables: make([]*DrawDBTable, 0),
Relationships: make([]*DrawDBRelationship, 0),
Notes: make([]*DrawDBNote, 0),
SubjectAreas: make([]*DrawDBArea, 0),
}
tableID := 0
fieldID := 0
gridX, gridY := 50, 50
colWidth, rowHeight := 300, 200
tablesPerRow := 4
for idx, table := range schema.Tables {
drawTable, newFieldID := w.convertTableToDrawDB(table, schema.Name, tableID, fieldID, idx, tablesPerRow, gridX, gridY, colWidth, rowHeight, 0)
drawSchema.Tables = append(drawSchema.Tables, drawTable)
fieldID = newFieldID
tableID++
}
return drawSchema
}
// tableToDrawDB converts a single Table to DrawDB format
func (w *Writer) tableToDrawDB(table *models.Table) *DrawDBSchema {
drawSchema := &DrawDBSchema{
Tables: make([]*DrawDBTable, 0),
Relationships: make([]*DrawDBRelationship, 0),
Notes: make([]*DrawDBNote, 0),
SubjectAreas: make([]*DrawDBArea, 0),
}
drawTable, _ := w.convertTableToDrawDB(table, table.Schema, 0, 0, 0, 4, 50, 50, 300, 200, 0)
drawSchema.Tables = append(drawSchema.Tables, drawTable)
return drawSchema
}
// convertTableToDrawDB converts a table to DrawDB format and returns the table and next field ID
func (w *Writer) convertTableToDrawDB(table *models.Table, schemaName string, tableID, fieldID, tableIndex, tablesPerRow, gridX, gridY, colWidth, rowHeight, colorIndex int) (drawTable *DrawDBTable, nextFieldID int) {
// Calculate position
x := gridX + (tableIndex%tablesPerRow)*colWidth
y := gridY + (tableIndex/tablesPerRow)*rowHeight
drawTable = &DrawDBTable{
ID: tableID,
Name: table.Name,
Schema: schemaName,
Comment: table.Description,
Color: getColorForIndex(colorIndex),
X: x,
Y: y,
Fields: make([]*DrawDBField, 0),
Indexes: make([]*DrawDBIndex, 0),
}
// Add fields
for _, column := range table.Columns {
field := &DrawDBField{
ID: fieldID,
Name: column.Name,
Type: formatTypeForDrawDB(column),
Primary: column.IsPrimaryKey,
NotNull: column.NotNull,
Increment: column.AutoIncrement,
Comment: column.Comment,
}
if column.Default != nil {
field.Default = fmt.Sprintf("%v", column.Default)
}
// Check for unique constraint
for _, constraint := range table.Constraints {
if constraint.Type == models.UniqueConstraint {
for _, col := range constraint.Columns {
if col == column.Name {
field.Unique = true
break
}
}
}
}
drawTable.Fields = append(drawTable.Fields, field)
fieldID++
}
// Add indexes
indexID := 0
for _, index := range table.Indexes {
drawIndex := &DrawDBIndex{
ID: indexID,
Name: index.Name,
Unique: index.Unique,
Fields: make([]int, 0),
}
// Map column names to field IDs
for _, colName := range index.Columns {
for idx, field := range drawTable.Fields {
if field.Name == colName {
drawIndex.Fields = append(drawIndex.Fields, idx)
break
}
}
}
drawTable.Indexes = append(drawTable.Indexes, drawIndex)
indexID++
}
return drawTable, fieldID
}
// Helper functions
func formatTypeForDrawDB(column *models.Column) string {
typeStr := column.Type
if column.Length > 0 {
typeStr = fmt.Sprintf("%s(%d)", typeStr, column.Length)
} else if column.Precision > 0 {
if column.Scale > 0 {
typeStr = fmt.Sprintf("%s(%d,%d)", typeStr, column.Precision, column.Scale)
} else {
typeStr = fmt.Sprintf("%s(%d)", typeStr, column.Precision)
}
}
return typeStr
}
func getColorForIndex(index int) string {
colors := []string{
"#6366f1", // indigo
"#8b5cf6", // violet
"#ec4899", // pink
"#f43f5e", // rose
"#14b8a6", // teal
"#06b6d4", // cyan
"#0ea5e9", // sky
"#3b82f6", // blue
}
return colors[index%len(colors)]
}