# CRUD Operations with CloudBase

This document covers Create, Update, and Delete operations for CloudBase document database.

## Create Operations

### Adding a Single Document

Add a new document to a collection:

```javascript
// Add a single document
// Note: _openid is automatically added by SDK, do not include it in the data
const result = await db.collection('todos').add({
    title: 'Learn CloudBase',
    description: 'Study the database API',
    completed: false,
    priority: 'high',
    createdAt: new Date()
    // _openid is automatically populated from authenticated user session
});

console.log('Added document with ID:', result._id);
```

**Return Value:**
```javascript
{
    _id: "generated-doc-id",  // Auto-generated document ID
    // ... other metadata
}
```

### Adding with Custom ID

Specify your own document ID:

```javascript
// Add with custom ID
const result = await db.collection('todos')
    .doc('custom-todo-id')
    .set({
        title: 'Custom ID Todo',
        completed: false,
        createdAt: new Date()
    });
```

**Note:** Use `.set()` with `.doc()` to specify a custom ID. If document exists, it will be overwritten.

### Adding Multiple Documents

Add multiple documents at once:

```javascript
// Batch add documents
const todos = [
    { title: 'Task 1', completed: false },
    { title: 'Task 2', completed: false },
    { title: 'Task 3', completed: true }
];

// Add one by one
for (const todo of todos) {
    await db.collection('todos').add(todo);
}

// Or use Promise.all for parallel insertion
const results = await Promise.all(
    todos.map(todo => db.collection('todos').add(todo))
);

console.log('Added', results.length, 'documents');
```

### Data Validation

Validate data before insertion:

```javascript
function validateTodo(todo) {
    if (!todo.title || todo.title.trim() === '') {
        throw new Error('Title is required');
    }
    if (typeof todo.completed !== 'boolean') {
        throw new Error('Completed must be a boolean');
    }
    return true;
}

async function addTodo(todoData) {
    try {
        validateTodo(todoData);
        
        const result = await db.collection('todos').add({
            ...todoData,
            createdAt: new Date(),
            updatedAt: new Date()
        });
        
        return result;
    } catch (error) {
        console.error('Failed to add todo:', error);
        throw error;
    }
}
```

## Update Operations

### Update by Document ID

Update a specific document by its ID:

```javascript
// Update by ID
// Note: Do not include _openid in update data - it cannot be modified
const result = await db.collection('todos')
    .doc('todo-id-123')
    .update({
        completed: true,
        updatedAt: new Date()
        // _openid cannot be updated and should not be included
    });

console.log('Updated:', result.updated, 'document(s)');
```

**Return Value:**
```javascript
{
    updated: 1,  // Number of documents updated
    // ... other metadata
}
```

**Important for permission-sensitive updates:**

- Treat the update as successful only when `result.updated > 0`.
- If the SDK returns a result object with fields such as `code` or `message`, surface that as an error instead of navigating as if the save succeeded.
- For simple owner-only collections, be careful with `.doc(id).update()` when your rule depends on non-_id fields. But for CMS-style article collections that use the validated app-role pattern `get('database.user_roles.' + auth.uid).role == 'admin' || doc.authorId == auth.uid`, `.doc(id).update()` / `.doc(id).remove()` is an acceptable path.

```javascript
const result = await db.collection('posts')
  .doc(postId)
  .update({
    title: nextTitle,
    updatedAt: new Date()
  });

if (result.code || result.updated !== 1) {
  throw new Error(result.message || 'Update was rejected by security rules');
}
```

### Update with Conditions

Update documents matching specific conditions:

```javascript
// Update all incomplete high-priority todos
const result = await db.collection('todos')
    .where({
        completed: false,
        priority: 'high'
    })
    .update({
        priority: 'urgent',
        updatedAt: new Date()
    });

console.log('Updated', result.updated, 'documents');
```

Use this form when your security rule depends on non-ID fields that you can include directly in the query conditions.

### Partial Updates

Only update specific fields (other fields remain unchanged):

```javascript
// Only update the title, leave other fields unchanged
// Note: _openid cannot be updated and should not be included
await db.collection('todos')
    .doc('todo-id-123')
    .update({
        title: 'Updated Title'
        // _openid remains unchanged and cannot be modified
    });
```

### Owner Rules for `.doc(id).update()` / `.doc(id).remove()`

CloudBase security rules can be tricky around document-ID writes. For many owner-only patterns, `.doc(id)` plus `doc.authorId` is fragile. But in the CMS article pattern validated by this evaluation loop, the collection uses a `CUSTOM` rule with app-role override:

```json
{
  "read": "auth.uid != null",
  "create": "auth.uid != null",
  "update": "auth.uid != null && (get('database.user_roles.' + auth.uid).role == 'admin' || doc.authorId == auth.uid)",
  "delete": "auth.uid != null && (get('database.user_roles.' + auth.uid).role == 'admin' || doc.authorId == auth.uid)"
}
```

With that rule shape, `.doc(id).update()` / `.doc(id).remove()` is a validated implementation path for CMS-style article management.

**Problematic rule for document-ID writes:**

```javascript
{
  "update": "auth.uid == doc.authorId",
  "delete": "auth.uid == doc.authorId"
}
```

This can work for `where({ authorId: auth.uid }).update(...)`, but it is commonly rejected for `.doc(id).update(...)` and `.doc(id).remove()`.

**Prefer simple permission when it already matches the product requirement:**

If the collection only needs “public read, creator/admin write”, use the simple permission `READONLY` instead of a CUSTOM owner rule.

**If you must keep a CUSTOM owner rule:**

```javascript
{
  "update": "doc.authorId == auth.uid",
  "delete": "doc.authorId == auth.uid"
}
```

Then change the client write path to include the owner field in the query condition:

```javascript
await db.collection('posts')
  .where({
    _id: postId,
    authorId: '{openid}'
  })
  .update({ title: 'Updated Title' });
```

Only use `get('database.user_roles.' + auth.uid)` or `get('database.users.' + auth.uid)` when that role collection's document `_id` is exactly the current `auth.uid`. If your users collection is queried by `where({ uid })`, then `get('database.users.' + auth.uid)` is not equivalent and will not resolve the same document. Do not treat `get('database.posts.' + doc._id)` as the default first-choice fix for owner writes.

### Nested Field Updates (Important)

When updating nested object fields, you **must use dot notation** if you want to preserve sibling fields.

**WRONG: This replaces the entire object and deletes sibling fields:**
```javascript
// DANGER: If 'user' had an 'email' field, it is now deleted!
await db.collection('profiles')
    .doc('profile-123')
    .update({
        user: {
            name: 'New Name'  // Replaces the ENTIRE 'user' object
        }
    });
```

**CORRECT: This only updates the specific nested field:**
```javascript
// SAFE: Only updates 'name', preserves 'email' and other fields in 'user'
await db.collection('profiles')
    .doc('profile-123')
    .update({
        'user.name': 'New Name'  // Use dot notation for nested fields
    });
```

### Update with Operators

Use update operators for complex updates:

```javascript
const _ = db.command;

// Increment a counter
await db.collection('posts')
    .doc('post-123')
    .update({
        views: _.inc(1)  // Increment views by 1
    });

// Add item to array
await db.collection('todos')
    .doc('todo-123')
    .update({
        tags: _.push(['urgent'])  // Add 'urgent' to tags array
    });

// Remove item from array
await db.collection('todos')
    .doc('todo-123')
    .update({
        tags: _.pull('completed')  // Remove 'completed' from tags
    });

// Multiply a number
await db.collection('products')
    .doc('product-123')
    .update({
        price: _.mul(1.1)  // Increase price by 10%
    });
```

### Common Update Operators

| Operator | Description | Example |
|----------|-------------|---------|
| `_.inc(n)` | Increment by n | `views: _.inc(1)` |
| `_.mul(n)` | Multiply by n | `price: _.mul(1.5)` |
| `_.push(items)` | Add to array | `tags: _.push(['new'])` |
| `_.pull(item)` | Remove from array | `tags: _.pull('old')` |
| `_.set(value)` | Set to value | `status: _.set('active')` |
| `_.remove()` | Remove field | `tempField: _.remove()` |

### Set vs Update

**`.update()`** - Updates only specified fields:
```javascript
// Only updates 'title', other fields remain unchanged
await db.collection('todos')
    .doc('todo-123')
    .update({ title: 'New Title' });
```

**`.set()`** - Replaces entire document:
```javascript
// Replaces entire document, removes unspecified fields
await db.collection('todos')
    .doc('todo-123')
    .set({ title: 'New Title', completed: false });
```

### Batch Updates

Update multiple documents efficiently:

```javascript
// Update all incomplete todos assigned to a user
async function reassignTodos(oldUserId, newUserId) {
    const result = await db.collection('todos')
        .where({
            assigneeId: oldUserId,
            completed: false
        })
        .update({
            assigneeId: newUserId,
            updatedAt: new Date()
        });
    
    return result.updated;
}

const updatedCount = await reassignTodos('user-1', 'user-2');
console.log('Reassigned', updatedCount, 'todos');
```

## Delete Operations

### Delete by Document ID

Delete a specific document:

```javascript
// Delete by ID
const result = await db.collection('todos')
    .doc('todo-id-123')
    .remove();

console.log('Deleted:', result.deleted, 'document(s)');
```

**Return Value:**
```javascript
{
    deleted: 1,  // Number of documents deleted
    // ... other metadata
}
```

### Delete with Conditions

Delete documents matching conditions:

```javascript
// Delete all completed todos older than 30 days
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);

const result = await db.collection('todos')
    .where({
        completed: true,
        completedAt: db.command.lt(thirtyDaysAgo)
    })
    .remove();

console.log('Deleted', result.deleted, 'old completed todos');
```

### Conditional Delete

Delete only if conditions are met:

```javascript
async function deleteTodoIfOwner(todoId, userId) {
    try {
        const result = await db.collection('todos')
            .where({
                _id: todoId,
                ownerId: userId  // Only delete if user is owner
            })
            .remove();
        
        if (result.deleted === 0) {
            throw new Error('Todo not found or user is not owner');
        }
        
        return true;
    } catch (error) {
        console.error('Delete failed:', error);
        return false;
    }
}
```

### Batch Delete

Delete multiple documents:

```javascript
// Delete all archived items
async function deleteArchived() {
    const result = await db.collection('todos')
        .where({
            status: 'archived'
        })
        .remove();
    
    return result.deleted;
}

const deletedCount = await deleteArchived();
console.log('Deleted', deletedCount, 'archived items');
```

### Soft Delete Pattern

Instead of permanently deleting, mark as deleted:

```javascript
// Soft delete - mark as deleted instead of removing
async function softDeleteTodo(todoId) {
    const result = await db.collection('todos')
        .doc(todoId)
        .update({
            deleted: true,
            deletedAt: new Date()
        });
    
    return result.updated > 0;
}

// Query only non-deleted items
async function getActiveTodos() {
    const result = await db.collection('todos')
        .where({
            deleted: db.command.neq(true)  // or: deleted: false
        })
        .get();
    
    return result.data;
}
```

## Complete CRUD Examples

### Todo Manager

```javascript
class TodoManager {
    constructor(db) {
        this.db = db;
        this.collection = db.collection('todos');
    }
    
    // Create
    async createTodo(title, description, priority = 'medium') {
        const result = await this.collection.add({
            title,
            description,
            priority,
            completed: false,
            createdAt: new Date(),
            updatedAt: new Date()
        });
        return result._id;
    }
    
    // Read (single)
    async getTodo(id) {
        const result = await this.collection.doc(id).get();
        return result.data[0];
    }
    
    // Read (multiple)
    async getTodos(filter = {}) {
        const result = await this.collection
            .where(filter)
            .orderBy('createdAt', 'desc')
            .get();
        return result.data;
    }
    
    // Update
    async updateTodo(id, updates) {
        const result = await this.collection
            .doc(id)
            .update({
                ...updates,
                updatedAt: new Date()
            });
        return result.updated > 0;
    }
    
    // Update status
    async toggleComplete(id) {
        const todo = await this.getTodo(id);
        return this.updateTodo(id, {
            completed: !todo.completed,
            completedAt: !todo.completed ? new Date() : null
        });
    }
    
    // Delete
    async deleteTodo(id) {
        const result = await this.collection.doc(id).remove();
        return result.deleted > 0;
    }
    
    // Batch operations
    async deleteCompleted() {
        const result = await this.collection
            .where({ completed: true })
            .remove();
        return result.deleted;
    }
}

// Usage
const todoManager = new TodoManager(db);

// Create
const todoId = await todoManager.createTodo(
    'Learn CloudBase', 
    'Study the database API', 
    'high'
);

// Read
const todo = await todoManager.getTodo(todoId);
const allTodos = await todoManager.getTodos({ completed: false });

// Update
await todoManager.updateTodo(todoId, { priority: 'urgent' });
await todoManager.toggleComplete(todoId);

// Delete
await todoManager.deleteTodo(todoId);
await todoManager.deleteCompleted();
```

## Error Handling Best Practices

```javascript
async function safeCRUD() {
    try {
        // Create
        const result = await db.collection('todos').add({
            title: 'New Todo'
        });
        
        console.log('Created:', result._id);
        
    } catch (error) {
        if (error.code === 'PERMISSION_DENIED') {
            console.error('No permission to create document');
        } else if (error.code === 'INVALID_PARAM') {
            console.error('Invalid data provided');
        } else {
            console.error('Unexpected error:', error);
        }
        
        throw error;  // Re-throw for caller to handle
    }
}
```

## Transaction Support

For operations requiring atomicity (all succeed or all fail):

```javascript
// Check CloudBase documentation for transaction API
// Transactions ensure data consistency
await db.runTransaction(async transaction => {
    // Read
    const todo = await transaction.collection('todos').doc('id').get();
    
    // Update based on read
    await transaction.collection('todos').doc('id').update({
        views: todo.data.views + 1
    });
});
```

## Best Practices

1. **Always handle errors**: Wrap operations in try-catch
2. **Validate input**: Check data before database operations
3. **Update timestamps**: Track createdAt and updatedAt
4. **Use transactions**: For related operations that must succeed together
5. **Batch operations**: Use batch updates/deletes when possible
6. **Soft deletes**: Consider soft delete for important data
7. **Index fields**: Index frequently queried/updated fields
8. **Limit updates**: Only update changed fields
9. **Configure security rules**: Use `managePermissions(action="updateResourcePermission")` to set database permissions before operations. See `./security-rules.md` for details. **Note:** Security rule changes take effect after a few minutes due to caching.
10. **Log operations**: Track important data changes

## Important: `_openid` Field Management

**CRITICAL: Never include `_openid` in write operations**

The `_openid` field is **automatically managed by the CloudBase SDK** and should **never** be included in any write operation data:

- **Automatic Assignment**: When you perform create, update, or set operations through the SDK, the system automatically writes the `_openid` field based on the current authenticated user's identity
- **Do Not Include**: The `_openid` field should **not** appear in any `data` parameter for write operations (`.add()`, `.update()`, `.set()`)
- **Error on Manual Setting**: If you manually include or modify `_openid` in write operations, the operation will **fail with an error**

**Correct Usage:**
```javascript
// Correct: Do not include _openid
await db.collection('todos').add({
    title: 'My Todo',
    completed: false
    // _openid is automatically added by SDK
});

// Wrong: Including _openid will cause an error
await db.collection('todos').add({
    title: 'My Todo',
    completed: false,
    _openid: 'some-id'  // ERROR: Cannot manually set _openid
});
```

**Note:** The `_openid` field is used internally by CloudBase for user identification and permission control. It is automatically populated from the authenticated user's session and cannot be manually overridden.
