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)] }