SPACEPORT DOCS

Documents

Documents are Spaceport's powerful ORM-like interface for interacting with your CouchDB database. They allow you to work with your data as native Groovy objects, abstracting away the complexities of JSON serialization and HTTP communication. This system enables you to define clear data models, encapsulate business logic directly within those models, and manage data persistence seamlessly.

The Document system is built on several interconnected components:

# Core Concepts

## The Document Class

The Document class is the foundation of Spaceport's data persistence layer. Every document has:

Documents can be used directly or extended to create custom document types with specific behavior and validation.

## CouchHandler and the Memory Core

CouchHandler is Spaceport's internal bridge to CouchDB. You typically interact with it through Spaceport.main_memory_core, which provides:

While most document operations use the Document class methods, Spaceport.main_memory_core is useful for database-level operations like checking if a database exists or creating new databases.

## Operations: Database Operation Results

Every database operation returns an Operation object that provides detailed feedback:

Operation op = document.save()

if (op.wasSuccessful()) {
    println "Document saved with revision: ${op.rev}"
} else {
    println "Save failed: ${op.error} - ${op.reason}"
}

Operation Properties:

Property Type Description
ok Boolean true if operation succeeded, false otherwise
id String Document ID (for successful operations)
rev String New revision ID (for save operations)
reason String Human-readable message about the operation
error String Error code or identifier (for failed operations)

Helper Methods:

# Working with Documents

## Fetching Documents

The Document class provides several static methods for retrieving documents from the database.

### get(id, database): Get or Create

The most common method. It retrieves an existing document or creates a new one if it doesn't exist.

import spaceport.computer.memory.physical.Document

// Get or create a document with ID 'app-settings' in the 'configuration' database
def settings = Document.get('app-settings', 'configuration')

// The document is now available for manipulation
settings.fields.theme = 'dark'
settings.fields.language = 'en'
settings.save()

When a document is created, an on document created alert is fired with this context:

[ 'id': id, 'database': database, 'doc': doc, 'classType': Document.class ]

### getIfExists(id, database): Fetch Only

Returns the document if it exists, or null if it doesn't. Does not create a new document.

def article = Document.getIfExists('my-article', 'articles')

if (article) {
    // Article exists, work with it
    println "Title: ${article.fields.title}"
} else {
    // Article doesn't exist, handle accordingly
    println "Article not found"
}

### getUncached(id, database): Force Fresh Fetch

Bypasses the document cache to ensure you get the latest version from the database. Useful when you know another process may have modified the document.

// Get the freshest version, ignoring any cached copy
def doc = Document.getUncached('user-123', 'users')

### getNew(database): Create with Random ID

Creates a new document with a randomly generated UUID.

// Create a new session document with a unique ID
def session = Document.getNew('sessions')

// The ID is automatically generated
println "New session ID: ${session._id}"

### exists(id, database): Check Existence

Checks if a document exists without retrieving its full contents.

if (Document.exists('admin', 'users')) {
    println "Admin user already exists"
} else {
    // Create the admin user
}

## Manipulating Document Data

Documents provide several properties for storing different types of data.

### The fields Map

The fields property is the primary container for your document's data. It's a standard Groovy Map intended for data that might be exposed to clients, so always sanitize user input.

def product = Document.get('product-123', 'products')

// Set fields directly
product.fields.name = 'Rocket Fuel'
product.fields.price = 1299.99
product.fields.inStock = true
product.fields.tags = ['fuel', 'premium', 'efficient']

// Access fields
def price = product.fields.price
def tags = product.fields.tags

// Save changes
product.save()

Best Practice: When storing user-submitted data, use Spaceport's .clean() method to sanitize HTML:

// Sanitize user input before storing
product.fields.description = userInput.clean()

### The cargo Property

Every document has a built-in Cargo object that provides advanced data manipulation features. Unlike fields, cargo is designed for internal application data and offers powerful methods for counters, toggles, sets, and more.

def article = Document.get('article-456', 'articles')

// Use cargo for counters
article.cargo.inc('viewCount')  // Increment view count
article.cargo.inc('likes', 5)   // Increment by 5

// Use cargo for toggles
article.cargo.toggle('featured')  // Toggle boolean value

// Use cargo for sets (unique lists)
article.cargo.addToSet('tags', 'groovy')
article.cargo.addToSet('tags', 'spaceport')

article.save()

See the Cargo Documentation for complete details on all available methods.

### The updates Map

The updates property automatically tracks document lifecycle events using timestamps as keys. Spaceport automatically adds entries for document creation and modification, but you can add your own custom entries.

def order = Document.get('order-789', 'orders')

// Spaceport automatically tracks creation
// order.updates[someTimestamp] = 'created'

// Add custom lifecycle events
order.updates[System.currentTimeMillis()] = [
    action: 'shipped',
    carrier: 'Cosmic Express',
    trackingNumber: 'CE123456789'
]

order.save()

// Later, review the history
order.updates.each { timestamp, event ->
    println "${new Date(timestamp as Long)}: ${event}"
}

### The type Property

Setting a type on your documents is a best practice for categorizing and querying different kinds of data within the same database.

def invoice = Document.get('inv-001', 'documents')
invoice.type = 'invoice'
invoice.fields.amount = 5000.00
invoice.save()

def receipt = Document.get('rec-001', 'documents')
receipt.type = 'receipt'
receipt.fields.amount = 5000.00
receipt.save()

// Later, you can query by type using Views

## Saving and Removing Documents

### save(): Persist Changes

The save() method persists all changes to the database and returns an Operation object.

def user = Document.get('user-alice', 'users')
user.fields.email = 'alice@example.com'
user.fields.lastLogin = System.currentTimeMillis()

// Save returns an Operation
Operation result = user.save()

if (result.wasSuccessful()) {
    println "Saved successfully. New revision: ${result.rev}"
    // The document's _rev is automatically updated
} else {
    println "Save failed: ${result.reason}"
}

Important Notes:

### Conflict Detection and Resolution

CouchDB uses revision IDs (_rev) to detect conflicts. If two processes try to update the same document simultaneously, the second save will fail with a conflict error.

// Process A fetches the document
def docA = Document.get('shared-doc', 'data')
docA.fields.value = 'A'

// Process B fetches the same document
def docB = Document.get('shared-doc', 'data')
docB.fields.value = 'B'

// Process A saves first (succeeds)
docA.save()

// Process B tries to save (conflict!)
Operation result = docB.save()
if (!result.wasSuccessful() && result.error == 'conflict') {
    // Handle the conflict
    // Option 1: Fetch fresh copy and retry
    docB = Document.getUncached('shared-doc', 'data')
    docB.fields.value = 'B'
    docB.save()
}

Spaceport fires an on document conflict alert during save operations when a conflict is detected, allowing you to implement custom conflict resolution strategies. See Handling Conflicts with Alerts.

### remove(): Delete Document

The remove() method permanently deletes a document from the database.

def tempDoc = Document.get('temp-123', 'temporary')

// Delete the document
Operation result = tempDoc.remove()

if (result.wasSuccessful()) {
    println "Document removed successfully"
}

Alerts Fired:

### close(): Remove from Cache

The close() method removes a document from Spaceport's internal cache. The next time the document is requested, it will be fetched fresh from the database.

def doc = Document.get('cached-doc', 'data')
doc.fields.value = 'modified'

// Remove from cache without saving
doc.close()

// Next fetch will get the original, unmodified version from the database
def fresh = Document.get('cached-doc', 'data')
// fresh.fields.value is still the old value

Use Case: This is useful when you've made changes you want to discard, or when you know another process has updated the document and you want to ensure you get the latest version.

# Custom Document Classes

Extending the Document class allows you to create strongly-typed data models with custom behavior, validation, and business logic.

## Creating a Custom Document Class

Here's a complete example of a custom document class for managing articles:

package documents

import spaceport.Spaceport
import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.Result
import spaceport.computer.memory.physical.Document

class ArticleDocument extends Document {
    
    // Ensure the articles database exists at startup
    @Alert('on initialize')
    static void _init(Result r) {
        if (!Spaceport.main_memory_core.containsDatabase('articles'))
            Spaceport.main_memory_core.createDatabase('articles')
    }
    
    // Custom static method for fetching by ID
    static ArticleDocument fetchById(String id) {
        return getIfExists(id, 'articles') as ArticleDocument
    }
    
    // Custom static method for creating new articles
    static ArticleDocument createNew(String title, String author) {
        def article = getNew('articles') as ArticleDocument
        article.type = 'article'
        article.title = title
        article.author = author
        article.published = false
        return article
    }
    
    // Tell Spaceport which additional properties to serialize
    def customProperties = ['title', 'author', 'body', 'tags', 'published', 'publishedDate']
    
    // Custom properties with getters and setters
    String title = 'Untitled'
    
    void setTitle(String title) {
        // Automatically sanitize titles
        this.title = title.clean()
    }
    
    String author = 'Anonymous'
    
    String body = ''
    
    void setBody(String body) {
        // Allow basic HTML in body content
        body.cleanType = 'simple'
        this.body = body.clean()
    }
    
    List<String> tags = []
    
    void addTag(String tag) {
        if (!tags.contains(tag)) {
            tags << tag.clean()
        }
    }
    
    void removeTag(String tag) {
        tags.remove(tag)
    }
    
    boolean published = false
    Long publishedDate = null
    
    void publish() {
        this.published = true
        this.publishedDate = System.currentTimeMillis()
        this.save()
    }
    
    void unpublish() {
        this.published = false
        this.publishedDate = null
        this.save()
    }
    
    // Custom business logic methods
    boolean isPublished() {
        return published && publishedDate != null
    }
    
    String getPublishedDateFormatted() {
        if (publishedDate) {
            return new Date(publishedDate).format('MMMM dd, yyyy')
        }
        return 'Not published'
    }
    
    int getWordCount() {
        return body.split(/\s+/).size()
    }
}

## Using Custom Document Classes

Once defined, custom document classes work just like the base Document class:

import documents.ArticleDocument

// Create a new article
def article = ArticleDocument.createNew(
    'Building with Spaceport',
    'Jeremy'
)

article.body = 'Spaceport is a comprehensive full-stack framework...'
article.addTag('spaceport')
article.addTag('groovy')
article.save()

// Fetch an existing article
def existing = ArticleDocument.fetchById('article-123')

if (existing) {
    println "Title: ${existing.title}"
    println "Author: ${existing.author}"
    println "Word Count: ${existing.wordCount}"
    println "Published: ${existing.publishedDateFormatted}"
    
    // Publish the article
    if (!existing.isPublished()) {
        existing.publish()
    }
}

## The customProperties List

The customProperties list tells Spaceport which additional properties should be serialized to the database. Without this, only the standard Document properties (type, fields, cargo, updates, etc.) would be saved.

class MyDocument extends Document {
    // These properties will be saved
    def customProperties = ['title', 'author', 'tags']
    
    String title
    String author
    List tags
    
    // This property will NOT be saved (not in customProperties)
    @JsonIgnore
    transient String tempCalculation
}

## Type Casting and Conversion

You can convert documents between types using Groovy's as operator:

// Fetch a generic document
def doc = Document.get('my-article', 'articles')

// Convert to specific type
def article = doc as ArticleDocument

// Or fetch directly as a specific type
def article2 = ArticleDocument.getIfExists('my-article', 'articles')

The asType() method handles type conversion and checks Spaceport's cache for existing typed instances.

# Views and ViewDocuments

CouchDB Views are a powerful way to query and aggregate data. Spaceport provides ViewDocument and View classes to work with them.

## Understanding CouchDB Views

Views are stored JavaScript functions in special _design documents. They consist of:

Views are indexed automatically and updated incrementally, making them extremely efficient for querying.

## Creating a ViewDocument

ViewDocument is a special document type that contains View definitions.

import spaceport.computer.memory.physical.ViewDocument

// Get or create a ViewDocument named 'articles'
// (stored as '_design/articles' in CouchDB)
def viewDoc = ViewDocument.get('articles', 'articles')

// Define a view that finds all published articles
viewDoc.setView('by-published', '''
    function(doc) {
        if (doc.type === 'article' && doc.published === true) {
            emit(doc.publishedDate, {
                title: doc.title,
                author: doc.author
            });
        }
    }
''')

// Define a view that counts articles by author
viewDoc.setView('count-by-author', 
    '''
    function(doc) {
        if (doc.type === 'article') {
            emit(doc.author, 1);
        }
    }
    ''',
    '_count'  // Built-in CouchDB reduce function
)

### setView() and setViewIfNeeded()

Both methods define or update a view, but with different save behavior:

// Always update and save
viewDoc.setView('my-view', mapFunction)

// Only save if changed (more efficient for startup scripts)
viewDoc.setViewIfNeeded('my-view', mapFunction)

// Chain multiple views before saving
viewDoc
    .setView('view-one', mapFunction1, null, false)
    .setView('view-two', mapFunction2, null, false)
    .setView('view-three', mapFunction3, null, true)  // Save on last one

## Querying Views

Once a view is defined, you can query it to retrieve results.

import spaceport.computer.memory.physical.View

// Get view results
def view = View.get('articles', 'by-published', 'articles')

// Access the rows
view.rows.each { row ->
    println "Published: ${row.key} - Title: ${row.value.title}"
}

// Get total count
println "Total published articles: ${view.total_rows}"

### Query Parameters

CouchDB supports many query parameters to filter and control view results:

// Get view with parameters
def view = View.get('articles', 'by-author', 'articles', [
    key: '"Jeremy"',              // Only articles by Jeremy
    limit: 10,                    // First 10 results
    descending: true,             // Reverse order
    skip: 5,                      // Skip first 5
    include_docs: true            // Include full documents
])

### Including Documents

By default, view results only include keys and values. To get the full documents:

// Get view with documents included
def view = View.getWithDocuments('articles', 'by-published', 'articles')

// Access the documents
List<Document> docs = view.getDocuments()

docs.each { doc ->
    println "Title: ${doc.fields.title}"
    println "Body: ${doc.fields.body}"
}

The getDocuments() method automatically deserializes the included documents into Document objects.

## View Properties

A View object contains:

Property Type Description
rows List Array of result rows, each with key, value, and optionally doc
total_rows Integer Total number of rows in the view (ignoring limit/skip)
offset Integer Number of rows skipped
database String The database this view is from (not serialized)

## Practical View Examples

### Finding Documents by Type

def viewDoc = ViewDocument.get('all', 'mydb')

viewDoc.setView('by-type', '''
    function(doc) {
        if (doc.type) {
            emit(doc.type, null);
        }
    }
''')

// Query for all 'user' documents
def users = View.get('all', 'by-type', 'mydb', [key: '"user"'])

### Aggregating Data with Reduce

def viewDoc = ViewDocument.get('orders', 'sales')

viewDoc.setView('total-by-month', 
    '''
    function(doc) {
        if (doc.type === 'order' && doc.date) {
            var date = new Date(doc.date);
            var month = date.getFullYear() + '-' + (date.getMonth() + 1);
            emit(month, doc.total);
        }
    }
    ''',
    '_sum'  // Sum all order totals by month
)

// Get totals
def view = View.get('orders', 'total-by-month', 'sales', [group: true])
view.rows.each { row ->
    println "Month: ${row.key}, Total: \$${row.value}"
}

### Complex Keys for Sorting

viewDoc.setView('by-status-and-date', '''
    function(doc) {
        if (doc.type === 'ticket') {
            emit([doc.status, doc.created], {
                title: doc.title,
                assignee: doc.assignee
            });
        }
    }
''')

// Get all 'open' tickets sorted by creation date
def openTickets = View.get('tickets', 'by-status-and-date', 'support', [
    startkey: '["open"]',
    endkey: '["open", {}]'
])

# Cargo Integration

Documents have deep integration with Spaceport's Cargo system, providing automatic persistence and reactivity.

## Built-in Document Cargo

Every document has a cargo property that works like any other Cargo object:

def doc = Document.get('stats', 'data')

// Use cargo methods
doc.cargo.set('hitCount', 0)
doc.cargo.inc('hitCount')
doc.cargo.toggle('active')
doc.cargo.addToSet('tags', 'important')

// Save the document to persist cargo changes
doc.save()

## Mirrored Cargo with Auto-Save

For automatic persistence, use Cargo.fromDocument() to create a mirrored Cargo that automatically saves changes:

import spaceport.computer.memory.virtual.Cargo
import spaceport.computer.memory.physical.Document

def doc = Document.get('article-meta', 'articles')

// Create a mirrored Cargo
def meta = Cargo.fromDocument(doc)

// Any changes are automatically saved asynchronously
meta.inc('viewCount')            // Auto-saves!
meta.addToSet('categories', 'tech')  // Auto-saves!
meta.set('featured', true)       // Auto-saves!

// No need to call doc.save() manually

See the Cargo documentation for complete details on mirrored Cargo objects.

## Reactive UIs with Document Cargo

Mirrored Cargo objects integrate with Launchpad's reactive system for real-time UI updates:

<%
    def article = ArticleDocument.fetchById('my-article')
    def meta = Cargo.fromDocument(article)
%>

<div class="article-stats">
    <span>Views: ${{ meta.getInteger('viewCount') }}</span>
    <button on-click=${ _{ meta.inc('likes') }}>
        Like (${{ meta.getInteger('likes') }})
    </button>
</div>

When a user clicks the Like button, the count increments on the server, auto-saves to the database, and the UI updates in real-time.

# Document Lifecycle Alerts

Spaceport fires several alerts during document operations, allowing you to implement hooks for auditing, validation, cache invalidation, and more.

## Available Document Alerts

Alert String Timing Context Properties Description
on document created After creation id, database, doc, classType A new document was created
on document save Before save type, old, document, difference Before document is saved (can modify)
on document saved After save (async) type, old, document, difference, operation After successful save
on document modified After any change type, action, document, operation, difference, old After any modification
on document conflict During save conflict type, document, conflicted, changes, differences Conflict detected during save
on document remove Before deletion type, document Before document is deleted
on document removed After deletion type, document, operation After successful deletion

## Using Document Alerts

Here are practical examples of using document lifecycle alerts:

### Audit Logging

import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.Result
import spaceport.computer.memory.physical.Document

class AuditLogger {
    
    @Alert('on document modified')
    static _logChange(Result r) {
        def auditEntry = Document.getNew('audit-log')
        auditEntry.type = 'audit'
        auditEntry.fields.documentId = r.context.document._id
        auditEntry.fields.documentType = r.context.type
        auditEntry.fields.action = r.context.action  // 'saved' or 'removed'
        auditEntry.fields.timestamp = System.currentTimeMillis()
        auditEntry.fields.changes = r.context.difference
        auditEntry.save()
    }
}

### Cache Invalidation

class CacheManager {
    
    @Alert('on document saved')
    static _invalidateCache(Result r) {
        def doc = r.context.document
        
        // Clear cache entries for this document
        Cache.remove("doc:${doc._id}")
        
        // Clear related caches
        if (doc.type == 'article') {
            Cache.remove('articles:recent')
            Cache.remove('articles:by-author')
        }
    }
}

### Validation Before Save

class Validator {
    
    @Alert('on document save')
    static _validateArticle(Result r) {
        def doc = r.context.document
        
        if (doc.type == 'article') {
            // Validate required fields
            if (!doc.title || doc.title.trim().isEmpty()) {
                doc.title = 'Untitled'
            }
            
            if (!doc.author) {
                doc.author = 'Anonymous'
            }
            
            // Ensure tags is a list
            if (!(doc.tags instanceof List)) {
                doc.tags = []
            }
        }
    }
}

### Preventing Deletion

class DeletionGuard {
    
    @Alert(value = 'on document remove', priority = 100)
    static _preventCriticalDeletion(Result r) {
        def doc = r.context.document
        
        // Prevent deletion of protected documents
        if (doc.fields?.protected == true) {
            println "Attempted to delete protected document: ${doc._id}"
            r.cancelled = true  // Stop the deletion
        }
    }
}

## Handling Conflicts with Alerts

The on document conflict alert fires during save operations when CouchDB detects a conflict. This allows you to implement custom conflict resolution strategies:

class ConflictResolver {
    
    @Alert('on document conflict')
    static _resolveConflict(Result r) {
        def current = r.context.document      // Your version
        def conflicted = r.context.conflicted // Database version
        def changes = r.context.changes       // Your changes
        def differences = r.context.differences  // What's different
        
        // Strategy 1: Always keep our changes (overwrite theirs)
        current._rev = conflicted._rev
        // Changes will be applied on retry
        
        // Strategy 2: Merge changes intelligently
        if (current.type == 'counter') {
            // For counters, add the values instead of replacing
            def ourCount = current.fields.count
            def theirCount = conflicted.fields.count
            def originalCount = ourCount - (changes.count?.new ?: 0)
            
            current.fields.count = originalCount + (theirCount - originalCount) + (changes.count?.new ?: 0)
            current._rev = conflicted._rev
        }
        
        // Strategy 3: Take their version and discard ours
        // (Just let the save fail, and fetch fresh copy)
    }
}

# Document State Management

Documents include a states property for tracking state history over time. This is an advanced feature for workflows and state machines.

## Setting and Getting State

def order = Document.get('order-123', 'orders')

// Set a state with value and details
order.setState('fulfillment', 'processing', [
    warehouse: 'West Coast',
    estimatedShip: '2025-11-01'
])

// Or just a simple value
order.setState('payment', 'paid')

// Get current state
def fulfillmentState = order.getState('fulfillment')
println "Status: ${fulfillmentState.value}"
println "Details: ${fulfillmentState.details}"

// Get state history
def history = order.getStateHistory('fulfillment')
history.each { timestamp, state ->
    println "At ${new Date(timestamp as Long)}: ${state.value}"
}

order.save()

## State with Closures

You can use closures to update state based on previous state:

order.setState('progress') { previousState ->
    def currentPercent = previousState.value ?: 0
    return [
        value: currentPercent + 25,
        details: [step: 'Next stage complete']
    ]
}

# Database Management

While most operations use the Document class, you can access Spaceport.main_memory_core (a CouchHandler instance) directly for database-level operations.

## Creating and Deleting Databases

import spaceport.Spaceport

// Check if database exists
if (!Spaceport.main_memory_core.containsDatabase('mydb')) {
    // Create database
    def op = Spaceport.main_memory_core.createDatabase('mydb')
    if (op.wasSuccessful()) {
        println "Database 'mydb' created"
    }
}

// Delete a database (careful!)
def op = Spaceport.main_memory_core.deleteDatabase('tempdb')
if (op.wasSuccessful()) {
    println "Database 'tempdb' deleted"
}

## Querying All Documents

// Get all document IDs in a database
def view = Spaceport.main_memory_core.getAll('mydb')

view.rows.each { row ->
    println "Document ID: ${row.id}"
}

// Get all documents with their contents
def view = Spaceport.main_memory_core.getAll('mydb', [include_docs: true])

def docs = view.getDocuments()
docs.each { doc ->
    println "Document: ${doc._id}, Type: ${doc.type}"
}

# Attachments

Documents can have file attachments stored in the _attachments property. Attachments are stored in base64 format for inline attachments, or can be fetched as streams for large files.

## Working with Attachments

// Get an attachment
def outputStream = new ByteArrayOutputStream()
def metadata = Spaceport.main_memory_core.getDocAttachment(
    'profile-pic.jpg',  // Attachment name
    'user-123',         // Document ID
    'users',            // Database
    outputStream        // Where to write the data
)

println "Content type: ${metadata.content_type}"
println "Content length: ${metadata.content_length}"

// The attachment data is now in outputStream
byte[] imageData = outputStream.toByteArray()

# Performance and Caching

## Document Caching

Spaceport automatically caches documents for performance. The cache key is "${_id}/${database}".

// First fetch - goes to database
def doc1 = Document.get('my-doc', 'mydb')

// Second fetch - returns cached copy (instant)
def doc2 = Document.get('my-doc', 'mydb')

// doc1 and doc2 are the same object instance
assert doc1.is(doc2)

// Force a fresh fetch
def doc3 = Document.getUncached('my-doc', 'mydb')

// Manually clear from cache
doc1.close()

Cache Behavior:

## When to Use getUncached()

Use getUncached() when:

For most use cases, the standard get() method's caching provides better performance without issues.

# Best Practices

## General Guidelines

1. Always set a type property for documents to enable easy querying and filtering 2. Use fields for client-exposed data and sanitize user input with .clean() 3. Use cargo for internal application data and leverage its rich methods 4. Track important events in the updates map with timestamps 5. Create custom document classes for complex data models with behavior 6. Use Views instead of loading all documents and filtering in code 7. Leverage Alerts for cross-cutting concerns like auditing and validation

## Data Organization

// Good: Clear, structured data
def user = Document.get('user-123', 'users')
user.type = 'user'
user.fields.username = 'alice'
user.fields.email = 'alice@example.com'
user.cargo.inc('loginCount')
user.updates[System.currentTimeMillis()] = 'created'

// Avoid: Mixing concerns, unclear structure
def user = Document.get('user-123', 'users')
user.fields.data = [
    user: 'alice',
    info: ['a@e.com', 0],
    misc: [:]
]

## Custom Document Classes

// Good: Clear responsibilities, encapsulated logic
class UserDocument extends Document {
    def customProperties = ['username', 'email', 'role']
    
    String username
    String email
    String role = 'user'
    
    boolean isAdmin() {
        return role == 'admin'
    }
    
    void promoteToAdmin() {
        this.role = 'admin'
        this.save()
    }
}

// Avoid: Accessing fields directly, no encapsulation
def user = Document.get('user', 'users')
if (user.fields.role == 'admin') {
    // Complex logic without helper methods
}

## Error Handling

// Good: Check operation results
def doc = Document.get('data', 'mydb')
doc.fields.value = 'new value'

def result = doc.save()
if (!result.wasSuccessful()) {
    println "Save failed: ${result.error} - ${result.reason}"
    
    if (result.error == 'conflict') {
        // Handle conflict
        doc = Document.getUncached('data', 'mydb')
        doc.fields.value = 'new value'
        doc.save()
    }
}

// Avoid: Ignoring operation results
doc.save()  // Did it work? Who knows!

# Common Patterns

## Singleton Configuration Documents

class AppSettings {
    private static settings = Document.get('app-settings', 'config')
    
    static String get(String key) {
        return settings.fields[key]
    }
    
    static void set(String key, value) {
        settings.fields[key] = value
        settings.save()
    }
}

// Usage
AppSettings.set('theme', 'dark')
def theme = AppSettings.get('theme')

## Document Collections with Views

class Articles {
    static {
        // Setup views at startup
        def viewDoc = ViewDocument.get('articles', 'articles')
        viewDoc.setViewIfNeeded('published', '''
            function(doc) {
                if (doc.type === 'article' && doc.published) {
                    emit(doc.publishedDate, null);
                }
            }
        ''')
    }
    
    static List<ArticleDocument> getPublished(int limit = 10) {
        def view = View.getWithDocuments('articles', 'published', 'articles', [
            limit: limit,
            descending: true
        ])
        return view.getDocuments().collect { it as ArticleDocument }
    }
    
    static ArticleDocument create(String title, String author) {
        def article = ArticleDocument.getNew('articles')
        article.type = 'article'
        article.title = title
        article.author = author
        article.save()
        return article
    }
}

## Soft Deletion

class SoftDeletable extends Document {
    def customProperties = ['deleted', 'deletedAt']
    
    boolean deleted = false
    Long deletedAt = null
    
    void softDelete() {
        this.deleted = true
        this.deletedAt = System.currentTimeMillis()
        this.save()
    }
    
    void restore() {
        this.deleted = false
        this.deletedAt = null
        this.save()
    }
}

// Setup a view to exclude deleted documents
viewDoc.setView('active', '''
    function(doc) {
        if (doc.type === 'user' && !doc.deleted) {
            emit(doc._id, null);
        }
    }
''')

# Troubleshooting

## Common Issues

Document Not Found:

def doc = Document.getIfExists('unknown', 'mydb')
if (!doc) {
    println "Document doesn't exist - use get() to create it"
    doc = Document.get('unknown', 'mydb')
}

Conflict Errors:

// Fetch the latest version and retry
doc = Document.getUncached(doc._id, doc.database)
doc.fields.value = newValue
doc.save()

Custom Properties Not Saving:

// Make sure to define customProperties in your class
class MyDoc extends Document {
    def customProperties = ['myField']  // Required!
    String myField
}

Cache Issues:

// Clear the cache if you suspect stale data
doc.close()
// Or force an uncached fetch
doc = Document.getUncached(id, database)

# Summary

The Document system provides a comprehensive ORM-like interface for CouchDB with:

# Next Steps

Now that you understand Documents, explore these related topics: