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
}