package graphql import ( "bufio" "fmt" "os" "regexp" "strings" "git.warky.dev/wdevs/relspecgo/pkg/models" "git.warky.dev/wdevs/relspecgo/pkg/readers" ) type Reader struct { options *readers.ReaderOptions } func NewReader(options *readers.ReaderOptions) *Reader { return &Reader{ options: options, } } func (r *Reader) ReadDatabase() (*models.Database, error) { if r.options.FilePath == "" { return nil, fmt.Errorf("file path is required for GraphQL reader") } content, err := os.ReadFile(r.options.FilePath) if err != nil { return nil, fmt.Errorf("failed to read file: %w", err) } return r.parseGraphQL(string(content)) } func (r *Reader) ReadSchema() (*models.Schema, error) { db, err := r.ReadDatabase() if err != nil { return nil, err } if len(db.Schemas) == 0 { return nil, fmt.Errorf("no schemas found") } return db.Schemas[0], nil } func (r *Reader) ReadTable() (*models.Table, error) { schema, err := r.ReadSchema() if err != nil { return nil, err } if len(schema.Tables) == 0 { return nil, fmt.Errorf("no tables found") } return schema.Tables[0], nil } type parseContext struct { inType bool inEnum bool currentType string typeLines []string currentEnum string enumLines []string customScalars map[string]bool } func (r *Reader) parseGraphQL(content string) (*models.Database, error) { dbName := "database" if r.options.Metadata != nil { if name, ok := r.options.Metadata["name"].(string); ok { dbName = name } } db := models.InitDatabase(dbName) schema := models.InitSchema("public") ctx := &parseContext{ customScalars: make(map[string]bool), } // First pass: collect custom scalars and enums scanner := bufio.NewScanner(strings.NewReader(content)) scalarRegex := regexp.MustCompile(`^\s*scalar\s+(\w+)`) enumRegex := regexp.MustCompile(`^\s*enum\s+(\w+)\s*\{`) closingBraceRegex := regexp.MustCompile(`^\s*\}`) for scanner.Scan() { line := scanner.Text() trimmed := strings.TrimSpace(line) if trimmed == "" || strings.HasPrefix(trimmed, "#") { continue } if matches := scalarRegex.FindStringSubmatch(trimmed); matches != nil { ctx.customScalars[matches[1]] = true continue } if matches := enumRegex.FindStringSubmatch(trimmed); matches != nil { ctx.inEnum = true ctx.currentEnum = matches[1] ctx.enumLines = []string{} continue } if closingBraceRegex.MatchString(trimmed) && ctx.inEnum { r.parseEnum(ctx.currentEnum, ctx.enumLines, schema) // Add enum name to custom scalars for type detection ctx.customScalars[ctx.currentEnum] = true ctx.inEnum = false ctx.currentEnum = "" ctx.enumLines = nil continue } if ctx.inEnum { ctx.enumLines = append(ctx.enumLines, line) } } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("scanner error: %w", err) } // Second pass: parse types scanner = bufio.NewScanner(strings.NewReader(content)) typeRegex := regexp.MustCompile(`^\s*type\s+(\w+)\s*\{`) ctx.inType = false ctx.inEnum = false for scanner.Scan() { line := scanner.Text() trimmed := strings.TrimSpace(line) if trimmed == "" || strings.HasPrefix(trimmed, "#") { continue } if matches := typeRegex.FindStringSubmatch(trimmed); matches != nil { ctx.inType = true ctx.currentType = matches[1] ctx.typeLines = []string{} continue } if closingBraceRegex.MatchString(trimmed) && ctx.inType { if err := r.parseType(ctx.currentType, ctx.typeLines, schema, ctx); err != nil { return nil, fmt.Errorf("failed to parse type %s: %w", ctx.currentType, err) } ctx.inType = false ctx.currentType = "" ctx.typeLines = nil continue } if ctx.inType { ctx.typeLines = append(ctx.typeLines, line) } } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("scanner error: %w", err) } db.Schemas = []*models.Schema{schema} // Third pass: detect and create relationships if err := r.detectAndCreateRelationships(schema, ctx); err != nil { return nil, fmt.Errorf("failed to create relationships: %w", err) } return db, nil } type fieldInfo struct { name string typeName string isArray bool isNullable bool innerNullable bool } func (r *Reader) parseType(typeName string, lines []string, schema *models.Schema, ctx *parseContext) error { table := models.InitTable(typeName, schema.Name) table.Metadata = make(map[string]any) // Store field info for relationship detection relationFields := make(map[string]*fieldInfo) fieldRegex := regexp.MustCompile(`^\s*(\w+)\s*:\s*(\[)?(\w+)(!)?(\])?(!)?\s*`) for _, line := range lines { trimmed := strings.TrimSpace(line) if trimmed == "" || strings.HasPrefix(trimmed, "#") { continue } matches := fieldRegex.FindStringSubmatch(trimmed) if matches == nil { continue } fieldName := matches[1] hasOpenBracket := matches[2] == "[" baseType := matches[3] innerNonNull := matches[4] == "!" hasCloseBracket := matches[5] == "]" outerNonNull := matches[6] == "!" isArray := hasOpenBracket && hasCloseBracket // Determine if this is a scalar or a relation if r.isScalarType(baseType, ctx) { // This is a scalar field column := models.InitColumn(fieldName, table.Name, schema.Name) column.Type = r.graphQLTypeToSQL(baseType, fieldName, typeName) if isArray { // Array of scalars: use array type column.Type += "[]" column.NotNull = outerNonNull } else { column.NotNull = !isArray && innerNonNull } // Check if this is a primary key (convention: field named "id") if fieldName == "id" { column.IsPrimaryKey = true column.AutoIncrement = true } table.Columns[fieldName] = column } else { // This is a relation field - store for later processing relationFields[fieldName] = &fieldInfo{ name: fieldName, typeName: baseType, isArray: isArray, isNullable: !innerNonNull && !isArray, innerNullable: !innerNonNull && isArray, } } } // Store relation fields in table metadata for relationship detection if len(relationFields) > 0 { table.Metadata["relationFields"] = relationFields } schema.Tables = append(schema.Tables, table) return nil } func (r *Reader) parseEnum(enumName string, lines []string, schema *models.Schema) { enum := &models.Enum{ Name: enumName, Schema: schema.Name, Values: make([]string, 0), } for _, line := range lines { trimmed := strings.TrimSpace(line) if trimmed == "" || strings.HasPrefix(trimmed, "#") { continue } // Enum values are simple identifiers enum.Values = append(enum.Values, trimmed) } schema.Enums = append(schema.Enums, enum) }