So far so good
Some checks are pending
CI / Test (1.23) (push) Waiting to run
CI / Test (1.24) (push) Waiting to run
CI / Test (1.25) (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Build (push) Waiting to run

This commit is contained in:
2025-12-16 18:10:40 +02:00
parent b9650739bf
commit 7c7054d2e2
44 changed files with 27029 additions and 48 deletions

View File

@@ -0,0 +1,77 @@
package drawdb
// DrawDBSchema represents the complete DrawDB JSON structure
type DrawDBSchema struct {
Tables []*DrawDBTable `json:"tables" yaml:"tables" xml:"tables"`
Relationships []*DrawDBRelationship `json:"relationships" yaml:"relationships" xml:"relationships"`
Notes []*DrawDBNote `json:"notes,omitempty" yaml:"notes,omitempty" xml:"notes,omitempty"`
SubjectAreas []*DrawDBArea `json:"subjectAreas,omitempty" yaml:"subjectAreas,omitempty" xml:"subjectAreas,omitempty"`
}
// DrawDBTable represents a table in DrawDB format
type DrawDBTable struct {
ID int `json:"id" yaml:"id" xml:"id"`
Name string `json:"name" yaml:"name" xml:"name"`
Schema string `json:"schema,omitempty" yaml:"schema,omitempty" xml:"schema,omitempty"`
Comment string `json:"comment,omitempty" yaml:"comment,omitempty" xml:"comment,omitempty"`
Color string `json:"color" yaml:"color" xml:"color"`
X int `json:"x" yaml:"x" xml:"x"`
Y int `json:"y" yaml:"y" xml:"y"`
Fields []*DrawDBField `json:"fields" yaml:"fields" xml:"fields"`
Indexes []*DrawDBIndex `json:"indexes,omitempty" yaml:"indexes,omitempty" xml:"indexes,omitempty"`
}
// DrawDBField represents a column/field in DrawDB format
type DrawDBField struct {
ID int `json:"id" yaml:"id" xml:"id"`
Name string `json:"name" yaml:"name" xml:"name"`
Type string `json:"type" yaml:"type" xml:"type"`
Default string `json:"default,omitempty" yaml:"default,omitempty" xml:"default,omitempty"`
Check string `json:"check,omitempty" yaml:"check,omitempty" xml:"check,omitempty"`
Primary bool `json:"primary" yaml:"primary" xml:"primary"`
Unique bool `json:"unique" yaml:"unique" xml:"unique"`
NotNull bool `json:"notNull" yaml:"notNull" xml:"notNull"`
Increment bool `json:"increment" yaml:"increment" xml:"increment"`
Comment string `json:"comment,omitempty" yaml:"comment,omitempty" xml:"comment,omitempty"`
}
// DrawDBIndex represents an index in DrawDB format
type DrawDBIndex struct {
ID int `json:"id" yaml:"id" xml:"id"`
Name string `json:"name" yaml:"name" xml:"name"`
Unique bool `json:"unique" yaml:"unique" xml:"unique"`
Fields []int `json:"fields" yaml:"fields" xml:"fields"` // Field IDs
}
// DrawDBRelationship represents a relationship in DrawDB format
type DrawDBRelationship struct {
ID int `json:"id" yaml:"id" xml:"id"`
Name string `json:"name" yaml:"name" xml:"name"`
StartTableID int `json:"startTableId" yaml:"startTableId" xml:"startTableId"`
EndTableID int `json:"endTableId" yaml:"endTableId" xml:"endTableId"`
StartFieldID int `json:"startFieldId" yaml:"startFieldId" xml:"startFieldId"`
EndFieldID int `json:"endFieldId" yaml:"endFieldId" xml:"endFieldId"`
Cardinality string `json:"cardinality" yaml:"cardinality" xml:"cardinality"` // "One to one", "One to many", "Many to one"
UpdateConstraint string `json:"updateConstraint,omitempty" yaml:"updateConstraint,omitempty" xml:"updateConstraint,omitempty"`
DeleteConstraint string `json:"deleteConstraint,omitempty" yaml:"deleteConstraint,omitempty" xml:"deleteConstraint,omitempty"`
}
// DrawDBNote represents a note in DrawDB format
type DrawDBNote struct {
ID int `json:"id" yaml:"id" xml:"id"`
Content string `json:"content" yaml:"content" xml:"content"`
Color string `json:"color" yaml:"color" xml:"color"`
X int `json:"x" yaml:"x" xml:"x"`
Y int `json:"y" yaml:"y" xml:"y"`
}
// DrawDBArea represents a subject area/grouping in DrawDB format
type DrawDBArea struct {
ID int `json:"id" yaml:"id" xml:"id"`
Name string `json:"name" yaml:"name" xml:"name"`
Color string `json:"color" yaml:"color" xml:"color"`
X int `json:"x" yaml:"x" xml:"x"`
Y int `json:"y" yaml:"y" xml:"y"`
Width int `json:"width" yaml:"width" xml:"width"`
Height int `json:"height" yaml:"height" xml:"height"`
}

View File

@@ -0,0 +1,349 @@
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
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++
}
}
// 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) (*DrawDBTable, 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)]
}