readysite / website / models / collection.go
5.2 KB
collection.go
package models

import (
	"encoding/json"
	"time"

	"github.com/readysite/readysite/pkg/database"
)

// CollectionType constants
const (
	CollectionTypeBase = "base" // Standard collection with documents
	CollectionTypeView = "view" // View collection backed by SQL query
)

// Collection represents a dynamic data schema (like PocketBase collections).
// ID can be set to a URL-friendly slug before insert (e.g., "blog_posts", "products").
type Collection struct {
	database.Model
	Name        string // Display name
	Description string // Collection description
	Type        string // "base" (default), "view", or "auth"
	Schema      string // JSON: array of field definitions (base type only)
	Query       string // SQL SELECT statement (view type only)
	ListRule    string // Rule for listing documents (empty = public)
	ViewRule    string // Rule for viewing single document
	CreateRule  string // Rule for creating documents (base type only)
	UpdateRule  string // Rule for updating documents (base type only)
	DeleteRule  string // Rule for deleting documents (base type only)
	RateLimit   string // JSON: rate limit config {requests, window, burst}
	System      bool   // System collections cannot be modified
}

// IsView returns true if this is a view collection.
func (c *Collection) IsView() bool {
	return c.Type == CollectionTypeView
}

// IsBase returns true if this is a base (standard) collection.
func (c *Collection) IsBase() bool {
	return c.Type == "" || c.Type == CollectionTypeBase
}

// Documents returns all documents in this collection.
func (c *Collection) Documents() ([]*Document, error) {
	return Documents.Search("WHERE CollectionID = ? ORDER BY CreatedAt DESC", c.ID)
}

// DocumentCount returns the number of documents in this collection.
func (c *Collection) DocumentCount() int {
	return Documents.Count("WHERE CollectionID = ?", c.ID)
}

// Rules returns ACL rules for this collection.
func (c *Collection) Rules() ([]*ACLRule, error) {
	return ACLRules.Search("WHERE ResourceType = 'collection' AND (ResourceID = ? OR ResourceID = '')", c.ID)
}

// --- Document ---

// Document represents a record in a collection.
type Document struct {
	database.Model
	CollectionID string // Collection this document belongs to
	Data         string // JSON: field values
}

// Collection returns the collection this document belongs to.
func (d *Document) Collection() (*Collection, error) {
	if d == nil {
		return nil, nil
	}
	return Collections.Get(d.CollectionID)
}

// Rules returns ACL rules for this document.
func (d *Document) Rules() ([]*ACLRule, error) {
	if d == nil {
		return nil, nil
	}
	return ACLRules.Search("WHERE ResourceType = 'document' AND (ResourceID = ? OR ResourceID = '')", d.ID)
}

// GetString returns a field value as a string.
func (d *Document) GetString(field string) string {
	if d == nil {
		return ""
	}
	data, err := d.data()
	if err != nil {
		return ""
	}
	if s, ok := data[field].(string); ok {
		return s
	}
	return ""
}

// GetInt returns a field value as an integer.
func (d *Document) GetInt(field string) int {
	if d == nil {
		return 0
	}
	data, err := d.data()
	if err != nil {
		return 0
	}
	switch v := data[field].(type) {
	case float64:
		return int(v)
	case int:
		return v
	case int64:
		return int(v)
	}
	return 0
}

// GetFloat returns a field value as a float64.
func (d *Document) GetFloat(field string) float64 {
	if d == nil {
		return 0
	}
	data, err := d.data()
	if err != nil {
		return 0
	}
	if f, ok := data[field].(float64); ok {
		return f
	}
	return 0
}

// GetBool returns a field value as a boolean.
func (d *Document) GetBool(field string) bool {
	if d == nil {
		return false
	}
	data, err := d.data()
	if err != nil {
		return false
	}
	if b, ok := data[field].(bool); ok {
		return b
	}
	return false
}

// GetTime returns a field value as a time.Time.
func (d *Document) GetTime(field string) time.Time {
	if d == nil {
		return time.Time{}
	}
	data, err := d.data()
	if err != nil {
		return time.Time{}
	}
	switch v := data[field].(type) {
	case string:
		t, _ := time.Parse(time.RFC3339, v)
		return t
	case time.Time:
		return v
	}
	return time.Time{}
}

// GetStrings returns a field value as a slice of strings.
func (d *Document) GetStrings(field string) []string {
	if d == nil {
		return nil
	}
	data, err := d.data()
	if err != nil {
		return nil
	}
	if arr, ok := data[field].([]any); ok {
		result := make([]string, 0, len(arr))
		for _, v := range arr {
			if s, ok := v.(string); ok {
				result = append(result, s)
			}
		}
		return result
	}
	return nil
}

// Set sets a field value.
func (d *Document) Set(field string, value any) error {
	if d == nil {
		return nil
	}
	data, err := d.data()
	if err != nil {
		data = make(map[string]any)
	}
	data[field] = value
	return d.setData(data)
}

// SetAll replaces all data.
func (d *Document) SetAll(data map[string]any) error {
	if d == nil {
		return nil
	}
	return d.setData(data)
}

func (d *Document) data() (map[string]any, error) {
	data := make(map[string]any)
	if d.Data == "" {
		return data, nil
	}
	err := json.Unmarshal([]byte(d.Data), &data)
	return data, err
}

func (d *Document) setData(data map[string]any) error {
	bytes, err := json.Marshal(data)
	if err != nil {
		return err
	}
	d.Data = string(bytes)
	return nil
}
← Back