Skip to main content

Document Operations

Complete guide to CRUD (Create, Read, Update, Delete) operations with Cocobase.

Table of Contents

Document Model

Document Structure

type Document struct {
ID string `json:"id"`
Collection string `json:"collection"`
Data map[string]interface{} `json:"data"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

Accessing Document Fields

doc, _ := client.GetDocument(ctx, "users", "doc-id")

// Built-in fields
fmt.Printf("ID: %s\n", doc.ID)
fmt.Printf("Collection: %s\n", doc.Collection)
fmt.Printf("Created: %v\n", doc.CreatedAt)
fmt.Printf("Updated: %v\n", doc.UpdatedAt)

// Custom data
name := doc.Data["name"].(string)
age := doc.Data["age"].(float64)
isActive := doc.Data["active"].(bool)

Create Documents

Basic Creation

data := map[string]interface{}{
"name": "Alice Smith",
"email": "alice@example.com",
"age": 28,
"active": true,
}

doc, err := client.CreateDocument(ctx, "users", data)
if err != nil {
log.Fatal(err)
}

fmt.Printf("Created document with ID: %s\n", doc.ID)

Creating with Different Data Types

data := map[string]interface{}{
// String
"name": "John Doe",

// Numbers
"age": 30,
"height": 5.9,

// Boolean
"active": true,

// Arrays
"tags": []string{"developer", "golang", "backend"},

// Nested objects
"address": map[string]interface{}{
"street": "123 Main St",
"city": "New York",
"zip": "10001",
},

// Timestamps
"registeredAt": time.Now().Format(time.RFC3339),
}

doc, err := client.CreateDocument(ctx, "users", data)

Error Handling

doc, err := client.CreateDocument(ctx, "users", data)
if err != nil {
if apiErr, ok := err.(*cocobase.APIError); ok {
switch apiErr.StatusCode {
case 400:
fmt.Println("Invalid data format")
case 401:
fmt.Println("Not authenticated")
case 403:
fmt.Println("Permission denied")
default:
fmt.Printf("Error: %s\n", apiErr.Suggestion)
}
} else {
log.Fatal(err)
}
}

Read Documents

Get Single Document

doc, err := client.GetDocument(ctx, "users", "document-id")
if err != nil {
if apiErr, ok := err.(*cocobase.APIError); ok {
if apiErr.StatusCode == 404 {
fmt.Println("Document not found")
return
}
}
log.Fatal(err)
}

fmt.Printf("Name: %s\n", doc.Data["name"])

Type Assertions

doc, err := client.GetDocument(ctx, "users", "document-id")
if err != nil {
log.Fatal(err)
}

// String
if name, ok := doc.Data["name"].(string); ok {
fmt.Printf("Name: %s\n", name)
}

// Number
if age, ok := doc.Data["age"].(float64); ok {
fmt.Printf("Age: %.0f\n", age)
}

// Boolean
if active, ok := doc.Data["active"].(bool); ok {
fmt.Printf("Active: %v\n", active)
}

// Array
if tags, ok := doc.Data["tags"].([]interface{}); ok {
for _, tag := range tags {
fmt.Printf("Tag: %s\n", tag)
}
}

// Nested object
if address, ok := doc.Data["address"].(map[string]interface{}); ok {
if city, ok := address["city"].(string); ok {
fmt.Printf("City: %s\n", city)
}
}

Safe Type Access Helper

func getString(data map[string]interface{}, key string) (string, bool) {
if val, ok := data[key].(string); ok {
return val, true
}
return "", false
}

func getFloat(data map[string]interface{}, key string) (float64, bool) {
if val, ok := data[key].(float64); ok {
return val, true
}
return 0, false
}

// Usage
doc, _ := client.GetDocument(ctx, "users", "doc-id")

if name, ok := getString(doc.Data, "name"); ok {
fmt.Printf("Name: %s\n", name)
}

if age, ok := getFloat(doc.Data, "age"); ok {
fmt.Printf("Age: %.0f\n", age)
}

Update Documents

Basic Update

updates := map[string]interface{}{
"age": 29,
"status": "active",
}

doc, err := client.UpdateDocument(ctx, "users", "document-id", updates)
if err != nil {
log.Fatal(err)
}

fmt.Printf("Updated document: %+v\n", doc.Data)

Partial Update

Updates are merged with existing data:

// Original document:
// {"name": "Alice", "age": 28, "city": "NYC"}

updates := map[string]interface{}{
"age": 29, // Updates existing field
}

doc, err := client.UpdateDocument(ctx, "users", "doc-id", updates)
// Result: {"name": "Alice", "age": 29, "city": "NYC"}

Adding New Fields

// Add new fields to existing document
updates := map[string]interface{}{
"phone": "+1234567890",
"website": "https://example.com",
}

doc, err := client.UpdateDocument(ctx, "users", "doc-id", updates)

Updating Nested Fields

// Update nested object
updates := map[string]interface{}{
"address": map[string]interface{}{
"city": "Los Angeles",
"state": "CA",
},
}

doc, err := client.UpdateDocument(ctx, "users", "doc-id", updates)

Updating Arrays

// Replace array
updates := map[string]interface{}{
"tags": []string{"golang", "backend", "microservices"},
}

doc, err := client.UpdateDocument(ctx, "users", "doc-id", updates)

Conditional Update

// Get current document
doc, err := client.GetDocument(ctx, "users", "doc-id")
if err != nil {
log.Fatal(err)
}

// Check condition
if status, ok := doc.Data["status"].(string); ok && status == "active" {
// Update only if active
updates := map[string]interface{}{
"lastModified": time.Now().Format(time.RFC3339),
}

doc, err = client.UpdateDocument(ctx, "users", doc.ID, updates)
if err != nil {
log.Fatal(err)
}
}

Delete Documents

Basic Deletion

err := client.DeleteDocument(ctx, "users", "document-id")
if err != nil {
if apiErr, ok := err.(*cocobase.APIError); ok {
if apiErr.StatusCode == 404 {
fmt.Println("Document not found")
return
}
}
log.Fatal(err)
}

fmt.Println("Document deleted successfully")

Instead of permanently deleting, mark documents as deleted:

// Soft delete by setting deletedAt field
updates := map[string]interface{}{
"deletedAt": time.Now().Format(time.RFC3339),
}

doc, err := client.UpdateDocument(ctx, "users", "doc-id", updates)
if err != nil {
log.Fatal(err)
}

fmt.Println("Document marked as deleted")

Query active (non-deleted) documents:

query := cocobase.NewQuery().
IsNull("deletedAt"). // Only non-deleted
Limit(100)

docs, err := client.ListDocuments(ctx, "users", query)

Or use the helper:

query := cocobase.NewQuery().
Active(). // Shortcut for IsNull("deletedAt")
Limit(100)

docs, err := client.ListDocuments(ctx, "users", query)

Conditional Deletion

// Get document first
doc, err := client.GetDocument(ctx, "users", "doc-id")
if err != nil {
log.Fatal(err)
}

// Check if can be deleted
if status, ok := doc.Data["status"].(string); ok {
if status != "protected" {
err = client.DeleteDocument(ctx, "users", doc.ID)
if err != nil {
log.Fatal(err)
}
fmt.Println("Document deleted")
} else {
fmt.Println("Cannot delete protected document")
}
}

List Documents

List All Documents

docs, err := client.ListDocuments(ctx, "users", nil)
if err != nil {
log.Fatal(err)
}

fmt.Printf("Found %d documents\n", len(docs))

for _, doc := range docs {
fmt.Printf("ID: %s, Name: %s\n", doc.ID, doc.Data["name"])
}

List with Query

query := cocobase.NewQuery().
Where("status", "active").
GreaterThanOrEqual("age", 18).
Limit(50)

docs, err := client.ListDocuments(ctx, "users", query)
if err != nil {
log.Fatal(err)
}

See the Query Builder Guide for advanced querying.

List with Raw Query String

// If you already have a query string
docs, err := client.QueryDocuments(ctx, "users", "status=active&age_gte=18&limit=50")
if err != nil {
log.Fatal(err)
}

Pagination

// Get page 2 with 20 items per page
query := cocobase.NewQuery().
Where("status", "active").
Page(2, 20).
Recent()

docs, err := client.ListDocuments(ctx, "users", query)

Iterate All Documents

func getAllDocuments(client *cocobase.Client, ctx context.Context, collection string) ([]cocobase.Document, error) {
var allDocs []cocobase.Document
page := 1
perPage := 100

for {
query := cocobase.NewQuery().
Page(page, perPage).
OrderBy("id") // Stable sort

docs, err := client.ListDocuments(ctx, collection, query)
if err != nil {
return nil, err
}

allDocs = append(allDocs, docs...)

// Stop if we got fewer than requested
if len(docs) < perPage {
break
}

page++
}

return allDocs, nil
}

Best Practices

1. Use Context for All Operations

// Good: Use context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

doc, err := client.CreateDocument(ctx, "users", data)

2. Handle Errors Appropriately

// Good: Check specific error types
doc, err := client.GetDocument(ctx, "users", id)
if err != nil {
if apiErr, ok := err.(*cocobase.APIError); ok {
if apiErr.StatusCode == 404 {
// Handle not found
return nil
}
}
return err
}

3. Validate Data Before Creating

// Good: Validate before sending
func createUser(client *cocobase.Client, name, email string, age int) error {
if name == "" {
return fmt.Errorf("name is required")
}
if !isValidEmail(email) {
return fmt.Errorf("invalid email")
}
if age < 0 {
return fmt.Errorf("age must be positive")
}

data := map[string]interface{}{
"name": name,
"email": email,
"age": age,
}

_, err := client.CreateDocument(ctx, "users", data)
return err
}

4. Use Type Assertions Safely

// Good: Check type before using
if name, ok := doc.Data["name"].(string); ok {
fmt.Printf("Name: %s\n", name)
} else {
fmt.Println("Name field is not a string or doesn't exist")
}

5. Prefer Soft Deletes

// Good: Use soft deletes for important data
updates := map[string]interface{}{
"deletedAt": time.Now().Format(time.RFC3339),
}

doc, err := client.UpdateDocument(ctx, "users", id, updates)

// Bad: Permanent deletion
// err := client.DeleteDocument(ctx, "users", id)

6. Use Pagination for Large Datasets

// Good: Use pagination
query := cocobase.NewQuery().
Where("status", "active").
Limit(100)

docs, err := client.ListDocuments(ctx, "users", query)

// Bad: Get all documents without limit
// docs, err := client.ListDocuments(ctx, "users", nil)

7. Set Reasonable Timeouts

// Good: Set timeout based on operation
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

doc, err := client.CreateDocument(ctx, "users", data)
// When updating related documents, consider doing it atomically
func transferOwnership(client *cocobase.Client, ctx context.Context, fromID, toID, itemID string) error {
// Get item
item, err := client.GetDocument(ctx, "items", itemID)
if err != nil {
return err
}

// Verify current owner
if item.Data["owner"] != fromID {
return fmt.Errorf("not the owner")
}

// Update item
updates := map[string]interface{}{
"owner": toID,
"transferredAt": time.Now().Format(time.RFC3339),
}

_, err = client.UpdateDocument(ctx, "items", itemID, updates)
return err
}

9. Batch Operations

// Process documents in batches
func processBatch(client *cocobase.Client, ctx context.Context) error {
query := cocobase.NewQuery().
Where("status", "pending").
Limit(100)

docs, err := client.ListDocuments(ctx, "tasks", query)
if err != nil {
return err
}

for _, doc := range docs {
// Process each document
updates := map[string]interface{}{
"status": "processing",
"processedAt": time.Now().Format(time.RFC3339),
}

_, err := client.UpdateDocument(ctx, "tasks", doc.ID, updates)
if err != nil {
log.Printf("Failed to update %s: %v\n", doc.ID, err)
continue
}
}

return nil
}

10. Add Timestamps

// Good: Always include timestamps
data := map[string]interface{}{
"name": "Alice",
"email": "alice@example.com",
"createdAt": time.Now().Format(time.RFC3339),
}

doc, err := client.CreateDocument(ctx, "users", data)

// On update
updates := map[string]interface{}{
"status": "active",
"updatedAt": time.Now().Format(time.RFC3339),
}

doc, err = client.UpdateDocument(ctx, "users", doc.ID, updates)

Previous: Getting Started | Next: Query Builder