cypher.go raw

   1  package neo4j
   2  
   3  import (
   4  	"context"
   5  	"fmt"
   6  	"regexp"
   7  	"strings"
   8  
   9  	"github.com/neo4j/neo4j-go-driver/v5/neo4j/dbtype"
  10  )
  11  
  12  // writeKeywords matches Cypher write operations. This is a safety net for the
  13  // owner-only endpoint, not a security boundary.
  14  var writeKeywords = regexp.MustCompile(`(?i)\b(CREATE|MERGE|SET|DELETE|REMOVE|DETACH|DROP|CALL)\b`)
  15  
  16  // stripCypherComments removes single-line (//) and block (/* */) comments
  17  // from a Cypher query string so they don't trigger false write-keyword matches.
  18  func stripCypherComments(q string) string {
  19  	// Block comments
  20  	for {
  21  		start := strings.Index(q, "/*")
  22  		if start < 0 {
  23  			break
  24  		}
  25  		end := strings.Index(q[start+2:], "*/")
  26  		if end < 0 {
  27  			q = q[:start]
  28  			break
  29  		}
  30  		q = q[:start] + q[start+2+end+2:]
  31  	}
  32  	// Single-line comments
  33  	lines := strings.Split(q, "\n")
  34  	for i, line := range lines {
  35  		if idx := strings.Index(line, "//"); idx >= 0 {
  36  			lines[i] = line[:idx]
  37  		}
  38  	}
  39  	return strings.Join(lines, "\n")
  40  }
  41  
  42  // validateReadOnly checks that a Cypher query contains no write operations.
  43  func validateReadOnly(cypher string) error {
  44  	stripped := stripCypherComments(cypher)
  45  	if writeKeywords.MatchString(stripped) {
  46  		return fmt.Errorf("query contains write operations; only read-only queries are allowed")
  47  	}
  48  	return nil
  49  }
  50  
  51  // ExecuteCypherRead executes a read-only Cypher query and returns the results
  52  // as a slice of maps. Each map represents one record with column names as keys.
  53  // This implements the store.CypherExecutor interface.
  54  func (n *N) ExecuteCypherRead(ctx context.Context, cypher string, params map[string]any) ([]map[string]any, error) {
  55  	if err := validateReadOnly(cypher); err != nil {
  56  		return nil, err
  57  	}
  58  
  59  	result, err := n.ExecuteRead(ctx, cypher, params)
  60  	if err != nil {
  61  		return nil, fmt.Errorf("cypher read failed: %w", err)
  62  	}
  63  
  64  	records := make([]map[string]any, 0, result.Len())
  65  	for result.Next(ctx) {
  66  		rec := result.Record()
  67  		row := make(map[string]any, len(rec.Keys))
  68  		for _, key := range rec.Keys {
  69  			val, _ := rec.Get(key)
  70  			row[key] = convertNeo4jValue(val)
  71  		}
  72  		records = append(records, row)
  73  	}
  74  	return records, nil
  75  }
  76  
  77  // convertNeo4jValue converts Neo4j driver types (Node, Relationship, Path)
  78  // into JSON-serializable plain maps and slices.
  79  func convertNeo4jValue(v any) any {
  80  	switch val := v.(type) {
  81  	case dbtype.Node:
  82  		m := map[string]any{
  83  			"_type":   "node",
  84  			"_id":     val.ElementId,
  85  			"_labels": val.Labels,
  86  		}
  87  		for k, pv := range val.Props {
  88  			m[k] = convertNeo4jValue(pv)
  89  		}
  90  		return m
  91  	case dbtype.Relationship:
  92  		return map[string]any{
  93  			"_type":     "relationship",
  94  			"_id":       val.ElementId,
  95  			"_relType":  val.Type,
  96  			"_startId":  val.StartElementId,
  97  			"_endId":    val.EndElementId,
  98  			"_props":    convertNeo4jProps(val.Props),
  99  		}
 100  	case dbtype.Path:
 101  		nodes := make([]any, len(val.Nodes))
 102  		for i, node := range val.Nodes {
 103  			nodes[i] = convertNeo4jValue(node)
 104  		}
 105  		rels := make([]any, len(val.Relationships))
 106  		for i, rel := range val.Relationships {
 107  			rels[i] = convertNeo4jValue(rel)
 108  		}
 109  		return map[string]any{
 110  			"_type":          "path",
 111  			"_nodes":         nodes,
 112  			"_relationships": rels,
 113  		}
 114  	case []any:
 115  		out := make([]any, len(val))
 116  		for i, item := range val {
 117  			out[i] = convertNeo4jValue(item)
 118  		}
 119  		return out
 120  	case map[string]any:
 121  		return convertNeo4jProps(val)
 122  	default:
 123  		return v
 124  	}
 125  }
 126  
 127  func convertNeo4jProps(props map[string]any) map[string]any {
 128  	out := make(map[string]any, len(props))
 129  	for k, v := range props {
 130  		out[k] = convertNeo4jValue(v)
 131  	}
 132  	return out
 133  }
 134  
 135  // Ensure N implements CypherExecutor at compile time.
 136  var _ interface {
 137  	ExecuteCypherRead(ctx context.Context, cypher string, params map[string]any) ([]map[string]any, error)
 138  } = (*N)(nil)
 139