- 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.
396 lines
10 KiB
Go
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)]
|
|
}
|