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.
This commit is contained in:
149
docs/DOMAINS_DRAWDB.md
Normal file
149
docs/DOMAINS_DRAWDB.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# Domains and DrawDB Areas Integration
|
||||
|
||||
## Overview
|
||||
|
||||
Domains provide a way to organize tables from potentially multiple schemas into logical business groupings. When working with DrawDB format, domains are automatically imported/exported as **Subject Areas** - a native DrawDB feature for visually grouping tables.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Writing Domains to DrawDB (Export)
|
||||
|
||||
When you export a database with domains to DrawDB format:
|
||||
|
||||
1. **Schema Areas** are created automatically for each schema (existing behavior)
|
||||
2. **Domain Areas** are created for each domain, calculated based on the positions of the tables they contain
|
||||
3. The domain area bounds are automatically calculated to encompass all its tables with a small padding
|
||||
|
||||
```go
|
||||
// Example: Creating a domain and exporting to DrawDB
|
||||
db := models.InitDatabase("mydb")
|
||||
|
||||
// Create an "authentication" domain
|
||||
authDomain := models.InitDomain("authentication")
|
||||
authDomain.Tables = append(authDomain.Tables,
|
||||
models.InitDomainTable("users", "public"),
|
||||
models.InitDomainTable("roles", "public"),
|
||||
models.InitDomainTable("permissions", "public"),
|
||||
)
|
||||
db.Domains = append(db.Domains, authDomain)
|
||||
|
||||
// Create a "financial" domain spanning multiple schemas
|
||||
finDomain := models.InitDomain("financial")
|
||||
finDomain.Tables = append(finDomain.Tables,
|
||||
models.InitDomainTable("accounts", "public"),
|
||||
models.InitDomainTable("transactions", "public"),
|
||||
models.InitDomainTable("ledger", "finance"), // Different schema!
|
||||
)
|
||||
db.Domains = append(db.Domains, finDomain)
|
||||
|
||||
// Write to DrawDB - domains become subject areas
|
||||
writer := drawdb.NewWriter(&writers.WriterOptions{
|
||||
OutputPath: "schema.json",
|
||||
})
|
||||
writer.WriteDatabase(db)
|
||||
```
|
||||
|
||||
The resulting DrawDB JSON will have Subject Areas for both:
|
||||
- "authentication" area containing the auth tables
|
||||
- "financial" area containing the financial tables from both schemas
|
||||
|
||||
### Reading Domains from DrawDB (Import)
|
||||
|
||||
When you import a DrawDB file with Subject Areas:
|
||||
|
||||
1. **Subject Areas** are automatically converted to **Domains**
|
||||
2. Tables are assigned to a domain if they fall within the area's visual bounds
|
||||
3. Table references include both the table name and schema name
|
||||
|
||||
```go
|
||||
// Example: Reading DrawDB with areas
|
||||
reader := drawdb.NewReader(&readers.ReaderOptions{
|
||||
FilePath: "schema.json",
|
||||
})
|
||||
|
||||
db, err := reader.ReadDatabase()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Access domains
|
||||
for _, domain := range db.Domains {
|
||||
fmt.Printf("Domain: %s\n", domain.Name)
|
||||
for _, domainTable := range domain.Tables {
|
||||
fmt.Printf(" - %s.%s\n", domainTable.SchemaName, domainTable.TableName)
|
||||
|
||||
// Access the actual table reference if loaded
|
||||
if domainTable.RefTable != nil {
|
||||
fmt.Printf(" Description: %s\n", domainTable.RefTable.Description)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Domain Structure
|
||||
|
||||
```go
|
||||
type Domain struct {
|
||||
Name string // Domain name (e.g., "authentication", "user_data")
|
||||
Description string // Optional human-readable description
|
||||
Tables []*DomainTable // Tables belonging to this domain
|
||||
Comment string // Optional comment
|
||||
Metadata map[string]any // Extensible metadata
|
||||
Sequence uint // Ordering hint
|
||||
}
|
||||
|
||||
type DomainTable struct {
|
||||
TableName string // Table name
|
||||
SchemaName string // Schema containing the table
|
||||
Sequence uint // Ordering hint
|
||||
RefTable *Table // Pointer to actual table (in-memory only, not serialized)
|
||||
}
|
||||
```
|
||||
|
||||
## Multi-Schema Domains
|
||||
|
||||
One of the key features of domains is that they can span multiple schemas:
|
||||
|
||||
```
|
||||
Domain: "user_data"
|
||||
├── public.users
|
||||
├── public.profiles
|
||||
├── public.user_preferences
|
||||
├── auth.user_sessions
|
||||
└── auth.mfa_devices
|
||||
```
|
||||
|
||||
This allows you to organize related tables even when they're stored in different schemas.
|
||||
|
||||
## Visual Organization in DrawDB
|
||||
|
||||
When viewing the exported DrawDB file in DrawDB Editor:
|
||||
|
||||
1. **Schema areas** appear in one color (original behavior)
|
||||
2. **Domain areas** appear in a different color
|
||||
3. Domain area bounds are calculated to fit all contained tables
|
||||
4. Areas can overlap - a table can visually belong to multiple areas
|
||||
|
||||
## Integration with Other Formats
|
||||
|
||||
Currently, domain/area integration is implemented for DrawDB format.
|
||||
|
||||
To implement similar functionality for other formats:
|
||||
|
||||
1. Identify if the format has a native grouping/area feature
|
||||
2. Add conversion logic in the reader to map format areas → Domain model
|
||||
3. Add conversion logic in the writer to map Domain model → format areas
|
||||
|
||||
Example formats that could support domains:
|
||||
- **DBML**: Could use DBML's `TableGroup` feature
|
||||
- **DrawDB**: ✅ Already implemented (Subject Areas)
|
||||
- **GraphQL**: Could use schema directives
|
||||
- **Custom formats**: Implement as needed
|
||||
|
||||
## Tips and Best Practices
|
||||
|
||||
1. **Keep domains focused**: Each domain should represent a distinct business area
|
||||
2. **Document purposes**: Use Description and Comment fields to explain each domain
|
||||
3. **Use meaningful names**: Domain names should clearly reflect their purpose
|
||||
4. **Maintain schema consistency**: Keep related tables together in the same schema when possible
|
||||
5. **Use metadata**: Store tool-specific information in the Metadata field
|
||||
@@ -44,12 +44,13 @@ The `--mode` flag controls how the template is executed:
|
||||
|------|-------------|--------|-------------|
|
||||
| `database` | Execute once for entire database | Single file | Documentation, reports, overview files |
|
||||
| `schema` | Execute once per schema | One file per schema | Schema-specific documentation |
|
||||
| `domain` | Execute once per domain | One file per domain | Domain-based documentation, domain exports |
|
||||
| `script` | Execute once per script | One file per script | Script processing |
|
||||
| `table` | Execute once per table | One file per table | Model generation, table docs |
|
||||
|
||||
### Filename Patterns
|
||||
|
||||
For multi-file modes (`schema`, `script`, `table`), use `--filename-pattern` to control output filenames:
|
||||
For multi-file modes (`schema`, `domain`, `script`, `table`), use `--filename-pattern` to control output filenames:
|
||||
|
||||
```bash
|
||||
# Default pattern
|
||||
@@ -296,6 +297,13 @@ The data available in templates depends on the execution mode:
|
||||
.Metadata // map[string]interface{} - User metadata
|
||||
```
|
||||
|
||||
### Domain Mode
|
||||
```go
|
||||
.Domain // *models.Domain - Current domain
|
||||
.ParentDatabase // *models.Database - Parent database context
|
||||
.Metadata // map[string]interface{} - User metadata
|
||||
```
|
||||
|
||||
### Table Mode
|
||||
```go
|
||||
.Table // *models.Table - Current table
|
||||
@@ -317,6 +325,7 @@ The data available in templates depends on the execution mode:
|
||||
**Database:**
|
||||
- `.Name` - Database name
|
||||
- `.Schemas` - List of schemas
|
||||
- `.Domains` - List of domains (business domain groupings)
|
||||
- `.Description`, `.Comment` - Documentation
|
||||
|
||||
**Schema:**
|
||||
@@ -325,6 +334,17 @@ The data available in templates depends on the execution mode:
|
||||
- `.Views`, `.Sequences`, `.Scripts` - Other objects
|
||||
- `.Enums` - Enum types
|
||||
|
||||
**Domain:**
|
||||
- `.Name` - Domain name
|
||||
- `.Tables` - List of DomainTable references
|
||||
- `.Description`, `.Comment` - Documentation
|
||||
- `.Metadata` - Custom metadata map
|
||||
|
||||
**DomainTable:**
|
||||
- `.TableName` - Name of the table
|
||||
- `.SchemaName` - Schema containing the table
|
||||
- `.RefTable` - Pointer to actual Table object (if loaded)
|
||||
|
||||
**Table:**
|
||||
- `.Name` - Table name
|
||||
- `.Schema` - Schema name
|
||||
|
||||
@@ -21,6 +21,7 @@ type Database struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Description string `json:"description,omitempty" yaml:"description,omitempty" xml:"description,omitempty"`
|
||||
Schemas []*Schema `json:"schemas" yaml:"schemas" xml:"schemas"`
|
||||
Domains []*Domain `json:"domains,omitempty" yaml:"domains,omitempty" xml:"domains,omitempty"`
|
||||
Comment string `json:"comment,omitempty" yaml:"comment,omitempty" xml:"comment,omitempty"`
|
||||
DatabaseType DatabaseType `json:"database_type,omitempty" yaml:"database_type,omitempty" xml:"database_type,omitempty"`
|
||||
DatabaseVersion string `json:"database_version,omitempty" yaml:"database_version,omitempty" xml:"database_version,omitempty"`
|
||||
@@ -32,6 +33,32 @@ func (d *Database) SQLName() string {
|
||||
return strings.ToLower(d.Name)
|
||||
}
|
||||
|
||||
// Domain represents a logical business domain grouping multiple tables from potentially different schemas.
|
||||
// Domains allow for organizing database tables by functional areas (e.g., authentication, user data, financial).
|
||||
type Domain struct {
|
||||
Name string `json:"name" yaml:"name" xml:"name"`
|
||||
Description string `json:"description,omitempty" yaml:"description,omitempty" xml:"description,omitempty"`
|
||||
Tables []*DomainTable `json:"tables" yaml:"tables" xml:"tables"`
|
||||
Comment string `json:"comment,omitempty" yaml:"comment,omitempty" xml:"comment,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty" xml:"-"`
|
||||
Sequence uint `json:"sequence,omitempty" yaml:"sequence,omitempty" xml:"sequence,omitempty"`
|
||||
}
|
||||
|
||||
// SQLName returns the domain name in lowercase for SQL compatibility.
|
||||
func (d *Domain) SQLName() string {
|
||||
return strings.ToLower(d.Name)
|
||||
}
|
||||
|
||||
// DomainTable represents a reference to a specific table within a domain.
|
||||
// It identifies the table by name and schema, allowing a single domain to include
|
||||
// tables from multiple schemas.
|
||||
type DomainTable struct {
|
||||
TableName string `json:"table_name" yaml:"table_name" xml:"table_name"`
|
||||
SchemaName string `json:"schema_name" yaml:"schema_name" xml:"schema_name"`
|
||||
Sequence uint `json:"sequence,omitempty" yaml:"sequence,omitempty" xml:"sequence,omitempty"`
|
||||
RefTable *Table `json:"-" yaml:"-" xml:"-"` // Excluded to prevent circular references
|
||||
}
|
||||
|
||||
// Schema represents a database schema, which is a logical grouping of database objects
|
||||
// such as tables, views, sequences, and relationships within a database.
|
||||
type Schema struct {
|
||||
@@ -295,6 +322,7 @@ func InitDatabase(name string) *Database {
|
||||
return &Database{
|
||||
Name: name,
|
||||
Schemas: make([]*Schema, 0),
|
||||
Domains: make([]*Domain, 0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,3 +430,20 @@ func InitSequence(name, schema string) *Sequence {
|
||||
StartValue: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// InitDomain initializes a new Domain with empty slices and maps
|
||||
func InitDomain(name string) *Domain {
|
||||
return &Domain{
|
||||
Name: name,
|
||||
Tables: make([]*DomainTable, 0),
|
||||
Metadata: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
// InitDomainTable initializes a new DomainTable reference
|
||||
func InitDomainTable(tableName, schemaName string) *DomainTable {
|
||||
return &DomainTable{
|
||||
TableName: tableName,
|
||||
SchemaName: schemaName,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +140,32 @@ func (r *Reader) convertToDatabase(drawSchema *drawdb.DrawDBSchema) (*models.Dat
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -127,6 +127,51 @@ func (w *Writer) databaseToDrawDB(d *models.Database) *DrawDBSchema {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -7,6 +7,7 @@ type TemplateData struct {
|
||||
// One of these will be populated based on execution mode
|
||||
Database *models.Database
|
||||
Schema *models.Schema
|
||||
Domain *models.Domain
|
||||
Script *models.Script
|
||||
Table *models.Table
|
||||
|
||||
@@ -57,6 +58,15 @@ func NewSchemaData(schema *models.Schema, metadata map[string]interface{}) *Temp
|
||||
}
|
||||
}
|
||||
|
||||
// NewDomainData creates template data for domain mode
|
||||
func NewDomainData(domain *models.Domain, db *models.Database, metadata map[string]interface{}) *TemplateData {
|
||||
return &TemplateData{
|
||||
Domain: domain,
|
||||
ParentDatabase: db,
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
// NewScriptData creates template data for script mode
|
||||
func NewScriptData(script *models.Script, schema *models.Schema, db *models.Database, metadata map[string]interface{}) *TemplateData {
|
||||
return &TemplateData{
|
||||
@@ -85,6 +95,9 @@ func (td *TemplateData) Name() string {
|
||||
if td.Schema != nil {
|
||||
return td.Schema.Name
|
||||
}
|
||||
if td.Domain != nil {
|
||||
return td.Domain.Name
|
||||
}
|
||||
if td.Script != nil {
|
||||
return td.Script.Name
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ const (
|
||||
DatabaseMode EntrypointMode = "database"
|
||||
// SchemaMode executes the template once per schema (multi-file output)
|
||||
SchemaMode EntrypointMode = "schema"
|
||||
// DomainMode executes the template once per domain (multi-file output)
|
||||
DomainMode EntrypointMode = "domain"
|
||||
// ScriptMode executes the template once per script (multi-file output)
|
||||
ScriptMode EntrypointMode = "script"
|
||||
// TableMode executes the template once per table (multi-file output)
|
||||
@@ -80,6 +82,8 @@ func (w *Writer) WriteDatabase(db *models.Database) error {
|
||||
return w.executeDatabaseMode(db)
|
||||
case SchemaMode:
|
||||
return w.executeSchemaMode(db)
|
||||
case DomainMode:
|
||||
return w.executeDomainMode(db)
|
||||
case ScriptMode:
|
||||
return w.executeScriptMode(db)
|
||||
case TableMode:
|
||||
@@ -143,6 +147,28 @@ func (w *Writer) executeSchemaMode(db *models.Database) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeDomainMode executes the template once per domain
|
||||
func (w *Writer) executeDomainMode(db *models.Database) error {
|
||||
for _, domain := range db.Domains {
|
||||
data := NewDomainData(domain, db, w.options.Metadata)
|
||||
output, err := w.executeTemplate(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute template for domain %s: %w", domain.Name, err)
|
||||
}
|
||||
|
||||
filename, err := w.generateFilename(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate filename for domain %s: %w", domain.Name, err)
|
||||
}
|
||||
|
||||
if err := w.writeOutput(output, filename); err != nil {
|
||||
return fmt.Errorf("failed to write output for domain %s: %w", domain.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeScriptMode executes the template once per script
|
||||
func (w *Writer) executeScriptMode(db *models.Database) error {
|
||||
for _, schema := range db.Schemas {
|
||||
|
||||
Reference in New Issue
Block a user