273 lines
6.0 KiB
Go
273 lines
6.0 KiB
Go
package graphql
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
|
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
|
)
|
|
|
|
type Writer struct {
|
|
options *writers.WriterOptions
|
|
}
|
|
|
|
func NewWriter(options *writers.WriterOptions) *Writer {
|
|
return &Writer{
|
|
options: options,
|
|
}
|
|
}
|
|
|
|
func (w *Writer) WriteDatabase(db *models.Database) error {
|
|
content := w.databaseToGraphQL(db)
|
|
|
|
if w.options.OutputPath != "" {
|
|
return os.WriteFile(w.options.OutputPath, []byte(content), 0644)
|
|
}
|
|
|
|
fmt.Print(content)
|
|
return nil
|
|
}
|
|
|
|
func (w *Writer) WriteSchema(schema *models.Schema) error {
|
|
db := models.InitDatabase(schema.Name)
|
|
db.Schemas = []*models.Schema{schema}
|
|
return w.WriteDatabase(db)
|
|
}
|
|
|
|
func (w *Writer) WriteTable(table *models.Table) error {
|
|
schema := models.InitSchema(table.Schema)
|
|
schema.Tables = []*models.Table{table}
|
|
db := models.InitDatabase(schema.Name)
|
|
db.Schemas = []*models.Schema{schema}
|
|
return w.WriteDatabase(db)
|
|
}
|
|
|
|
func (w *Writer) databaseToGraphQL(db *models.Database) string {
|
|
var sb strings.Builder
|
|
|
|
// Header comment
|
|
if w.shouldIncludeComments() {
|
|
sb.WriteString("# Generated GraphQL Schema\n")
|
|
if db.Name != "" {
|
|
sb.WriteString(fmt.Sprintf("# Database: %s\n", db.Name))
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
// Custom scalar declarations
|
|
if w.shouldIncludeScalarDeclarations() {
|
|
scalars := w.collectCustomScalars(db)
|
|
if len(scalars) > 0 {
|
|
for _, scalar := range scalars {
|
|
sb.WriteString(fmt.Sprintf("scalar %s\n", scalar))
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
// Enum definitions
|
|
for _, schema := range db.Schemas {
|
|
for _, enum := range schema.Enums {
|
|
sb.WriteString(w.enumToGraphQL(enum))
|
|
sb.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
// Type definitions
|
|
for _, schema := range db.Schemas {
|
|
for _, table := range schema.Tables {
|
|
// Skip join tables (tables with only PK+FK columns)
|
|
if w.isJoinTable(table) {
|
|
continue
|
|
}
|
|
|
|
sb.WriteString(w.tableToGraphQL(table, db, schema))
|
|
sb.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
func (w *Writer) shouldIncludeComments() bool {
|
|
if w.options.Metadata != nil {
|
|
if include, ok := w.options.Metadata["includeComments"].(bool); ok {
|
|
return include
|
|
}
|
|
}
|
|
return true // Default to true
|
|
}
|
|
|
|
func (w *Writer) shouldIncludeScalarDeclarations() bool {
|
|
if w.options.Metadata != nil {
|
|
if include, ok := w.options.Metadata["includeScalarDeclarations"].(bool); ok {
|
|
return include
|
|
}
|
|
}
|
|
return true // Default to true
|
|
}
|
|
|
|
func (w *Writer) collectCustomScalars(db *models.Database) []string {
|
|
scalarsNeeded := make(map[string]bool)
|
|
|
|
for _, schema := range db.Schemas {
|
|
for _, table := range schema.Tables {
|
|
for _, col := range table.Columns {
|
|
if scalar := w.sqlTypeToCustomScalar(col.Type); scalar != "" {
|
|
scalarsNeeded[scalar] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert to sorted slice
|
|
scalars := make([]string, 0, len(scalarsNeeded))
|
|
for scalar := range scalarsNeeded {
|
|
scalars = append(scalars, scalar)
|
|
}
|
|
sort.Strings(scalars)
|
|
|
|
return scalars
|
|
}
|
|
|
|
func (w *Writer) isJoinTable(table *models.Table) bool {
|
|
// A join table typically has:
|
|
// 1. Exactly 2 FK constraints
|
|
// 2. Composite primary key on those FK columns
|
|
// 3. No other columns
|
|
|
|
fkCount := 0
|
|
for _, constraint := range table.Constraints {
|
|
if constraint.Type == models.ForeignKeyConstraint {
|
|
fkCount++
|
|
}
|
|
}
|
|
|
|
if fkCount != 2 {
|
|
return false
|
|
}
|
|
|
|
// Check if all columns are either PKs or FKs
|
|
for _, col := range table.Columns {
|
|
isFKColumn := false
|
|
for _, constraint := range table.Constraints {
|
|
if constraint.Type == models.ForeignKeyConstraint {
|
|
for _, fkCol := range constraint.Columns {
|
|
if fkCol == col.Name {
|
|
isFKColumn = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !isFKColumn && !col.IsPrimaryKey {
|
|
// Found a column that's neither PK nor FK
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (w *Writer) enumToGraphQL(enum *models.Enum) string {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString(fmt.Sprintf("enum %s {\n", enum.Name))
|
|
for _, value := range enum.Values {
|
|
sb.WriteString(fmt.Sprintf(" %s\n", value))
|
|
}
|
|
sb.WriteString("}\n")
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
func (w *Writer) tableToGraphQL(table *models.Table, db *models.Database, schema *models.Schema) string {
|
|
var sb strings.Builder
|
|
|
|
// Type name
|
|
typeName := table.Name
|
|
|
|
// Description comment
|
|
if w.shouldIncludeComments() && (table.Description != "" || table.Comment != "") {
|
|
desc := table.Description
|
|
if desc == "" {
|
|
desc = table.Comment
|
|
}
|
|
sb.WriteString(fmt.Sprintf("# %s\n", desc))
|
|
}
|
|
|
|
sb.WriteString(fmt.Sprintf("type %s {\n", typeName))
|
|
|
|
// Collect and categorize fields
|
|
var idFields, scalarFields, relationFields []string
|
|
|
|
for _, column := range table.Columns {
|
|
// Skip FK columns (they become relation fields)
|
|
if w.isForeignKeyColumn(column, table) {
|
|
continue
|
|
}
|
|
|
|
gqlType := w.sqlTypeToGraphQL(column.Type, column, table, schema)
|
|
if gqlType == "" {
|
|
continue // Skip if type couldn't be mapped
|
|
}
|
|
|
|
// Determine nullability
|
|
if column.NotNull {
|
|
gqlType += "!"
|
|
}
|
|
|
|
field := fmt.Sprintf(" %s: %s", column.Name, gqlType)
|
|
|
|
if column.IsPrimaryKey {
|
|
idFields = append(idFields, field)
|
|
} else {
|
|
scalarFields = append(scalarFields, field)
|
|
}
|
|
}
|
|
|
|
// Add relation fields
|
|
relationFields = w.generateRelationFields(table, db, schema)
|
|
|
|
// Write fields in order: ID, scalars (sorted), relations (sorted)
|
|
for _, field := range idFields {
|
|
sb.WriteString(field + "\n")
|
|
}
|
|
|
|
sort.Strings(scalarFields)
|
|
for _, field := range scalarFields {
|
|
sb.WriteString(field + "\n")
|
|
}
|
|
|
|
if len(relationFields) > 0 {
|
|
if len(scalarFields) > 0 || len(idFields) > 0 {
|
|
sb.WriteString("\n") // Blank line before relations
|
|
}
|
|
sort.Strings(relationFields)
|
|
for _, field := range relationFields {
|
|
sb.WriteString(field + "\n")
|
|
}
|
|
}
|
|
|
|
sb.WriteString("}\n")
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
func (w *Writer) isForeignKeyColumn(column *models.Column, table *models.Table) bool {
|
|
for _, constraint := range table.Constraints {
|
|
if constraint.Type == models.ForeignKeyConstraint {
|
|
for _, fkCol := range constraint.Columns {
|
|
if fkCol == column.Name {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|