expand.go
package query
import (
"github.com/readysite/readysite/website/internal/content/schema"
"github.com/readysite/readysite/website/models"
)
// MaxExpansionDepth limits the recursion depth for nested expansions.
const MaxExpansionDepth = 3
// RelationExpander handles expanding relation fields in API responses.
type RelationExpander struct {
cache map[string]map[string]any // Cache for already-fetched documents
}
// NewExpander creates a new RelationExpander with an empty cache.
func NewExpander() *RelationExpander {
return &RelationExpander{
cache: make(map[string]map[string]any),
}
}
// ExpandRecord expands relation fields in a record based on expand paths.
// expandPaths is a list of paths like [["author"], ["comments", "author"]]
// depth starts at 1 and is limited by MaxExpansionDepth.
func (e *RelationExpander) ExpandRecord(record map[string]any, collection *models.Collection, expandPaths [][]string, depth int) {
if depth > MaxExpansionDepth || len(expandPaths) == 0 {
return
}
// Get schema fields
fields, err := schema.GetFields(collection)
if err != nil {
return
}
// Build field map
fieldMap := make(map[string]schema.Field)
for _, f := range fields {
fieldMap[f.Name] = f
}
// Process each expand path
expandData := make(map[string]any)
for _, path := range expandPaths {
if len(path) == 0 {
continue
}
fieldName := path[0]
field, ok := fieldMap[fieldName]
if !ok || field.Type != schema.Relation {
continue
}
// Get relation options
targetCollection := ""
isMultiple := false
if opts, ok := field.Options["collection"].(string); ok {
targetCollection = opts
}
if mult, ok := field.Options["multiple"].(bool); ok {
isMultiple = mult
}
if targetCollection == "" {
continue
}
// Get target collection for metadata
targetCol, err := models.Collections.Get(targetCollection)
if err != nil {
continue
}
// Get the field value (document ID or array of IDs)
val, ok := record[fieldName]
if !ok || val == nil {
continue
}
// Collect remaining path for nested expansion
var nestedPaths [][]string
if len(path) > 1 {
nestedPaths = append(nestedPaths, path[1:])
}
// Also check for other paths that start with the same field
for _, otherPath := range expandPaths {
if len(otherPath) > 1 && otherPath[0] == fieldName {
nestedPaths = append(nestedPaths, otherPath[1:])
}
}
if isMultiple {
// Multiple relation - val should be an array of IDs
var ids []string
switch v := val.(type) {
case []any:
for _, id := range v {
if s, ok := id.(string); ok {
ids = append(ids, s)
}
}
case []string:
ids = v
}
var expanded []map[string]any
for _, id := range ids {
expandedDoc := e.fetchAndExpandDocument(id, targetCol, nestedPaths, depth+1)
if expandedDoc != nil {
expanded = append(expanded, expandedDoc)
}
}
if len(expanded) > 0 {
expandData[fieldName] = expanded
}
} else {
// Single relation - val should be a document ID
id, ok := val.(string)
if !ok || id == "" {
continue
}
expandedDoc := e.fetchAndExpandDocument(id, targetCol, nestedPaths, depth+1)
if expandedDoc != nil {
expandData[fieldName] = expandedDoc
}
}
}
if len(expandData) > 0 {
record["expand"] = expandData
}
}
// fetchAndExpandDocument fetches a document and applies nested expansion.
func (e *RelationExpander) fetchAndExpandDocument(id string, collection *models.Collection, nestedPaths [][]string, depth int) map[string]any {
// Check cache
cacheKey := collection.ID + ":" + id
if cached, ok := e.cache[cacheKey]; ok {
return cached
}
// Fetch document
doc, err := models.Documents.Get(id)
if err != nil || doc.CollectionID != collection.ID {
return nil
}
// Convert to record
record := DocumentToRecord(doc, collection)
// Cache before nested expansion to handle cycles
e.cache[cacheKey] = record
// Apply nested expansion
if len(nestedPaths) > 0 && depth <= MaxExpansionDepth {
e.ExpandRecord(record, collection, nestedPaths, depth)
}
return record
}
// ExpandRecordSimple is a convenience function that creates an expander and expands a single record.
func ExpandRecordSimple(record map[string]any, collection *models.Collection, expandPaths [][]string) {
if len(expandPaths) == 0 {
return
}
expander := NewExpander()
expander.ExpandRecord(record, collection, expandPaths, 1)
}