文档数据库:Web SDK
Use CloudBase 文档数据库 Web SDK 进行数据操作
How to Use
See How to Use Prompts for detailed usage instructions.
Test Prompts
You can use the following prompts to test:
- "帮我创建一个待办事项应用,Use文档数据库存储数据"
- "实现分页查询和复杂查询功能"
Prompt
- rule
- aggregation
- complex-queries
- crud-operations
- geolocation
- pagination
- realtime
- security-rules
rule.md
# CloudBase Document Database Web SDK
This skill provides guidance on using the CloudBase document database Web SDK for data operations in web applications.
## Core Concepts
### Initialization
Before using any database operations, initialize the CloudBase SDK:
```javascript
import cloudbase from "@cloudbase/js-sdk";
// UMD version
// If you are not using npm, And want to use UMD version instead. You should refer to https://docs.cloudbase.net/quick-start/#web-%E5%BF%AB%E9%80%9F%E4%BD%93%E9%AA%8C for latest version of UMD version.
const app = cloudbase.init({
env: "your-env-id", // Replace with your environment id
});
const db = app.database();
const _ = db.command; // Get query operators
// ... login
```
Remember to sign in(auth) is ***REQUIRED** before actually querying the database.
### Collection Reference
Access collections using:
```javascript
db.collection('collection-name')
```
### Query Operators
CloudBase provides query operators via `db.command` (aliased as `_`):
- `_.gt(value)` - Greater than
- `_.gte(value)` - Greater than or equal
- `_.lt(value)` - Less than
- `_.lte(value)` - Less than or equal
- `_.eq(value)` - Equal to
- `_.neq(value)` - Not equal to
- `_.in(array)` - Value in array
- `_.nin(array)` - Value not in array
## Basic Operations
### Query Single Document
Query by document ID:
```javascript
const result = await db.collection('todos')
.doc('docId')
.get();
```
### Query Multiple Documents
Query with conditions:
```javascript
const result = await db.collection('todos')
.where({
completed: false,
priority: 'high'
})
.get();
```
**Note:** `get()` returns 100 records by default, maximum 1000.
### Query Methods Chaining
Combine methods for complex queries:
- `.where(conditions)` - Filter conditions
- `.orderBy(field, direction)` - Sort by field ('asc' or 'desc')
- `.limit(number)` - Limit results (default 100, max 1000)
- `.skip(number)` - Skip records for pagination
- `.field(object)` - Specify fields to return (true/false)
## Advanced Features
For detailed information on specific topics, refer to:
### CRUD Operations
See `./crud-operations.md` for:
- Creating documents (add, batch add)
- Updating documents (partial updates, operators)
- Deleting documents (conditional delete, soft delete)
- Complete CRUD manager examples
### Complex Queries
See `./complex-queries.md` for:
- Using query operators
- Combining multiple conditions
- Field selection
- Sorting and limiting results
### Pagination
See `./pagination.md` for:
- Implementing page-based navigation
- Calculating skip and limit values
- Cursor-based pagination
- Infinite scroll patterns
### Aggregation Queries
See `./aggregation.md` for:
- Grouping data
- Statistical calculations
- Pipeline operations
- Time-based aggregations
### Geolocation Queries
See `./geolocation.md` for:
- Proximity searches
- Area-based queries
- Geographic indexing requirements
- Distance-based features
### Realtime Database
See `./realtime.md` for:
- Real-time data synchronization using watch() method
- Setting up listeners for document changes
- Handling real-time updates in chat and collaboration apps
- Performance optimization and error handling
- Common patterns for real-time applications
### Security Rules
See `./security-rules.md` for:
- Configuring database permissions
- Simple permissions vs custom rules
- Permission categories and usage
- Security rule syntax and examples
## Common Patterns
### Error Handling
Always wrap database operations in try-catch:
```javascript
try {
const result = await db.collection('todos').get();
console.log(result.data);
} catch (error) {
console.error('Database error:', error);
}
```
### Return Value Structure
Database operations return:
```javascript
{
data: [...], // Array of documents
// Additional metadata
}
```
## Important Notes
1. **Environment ID**: Replace `"your-env-id"` with actual CloudBase environment ID
2. **Default Limits**: `get()` returns 100 records by default
3. **Collection Names**: Use string literals for collection names
4. **Geolocation Index**: Geographic queries require proper indexing
5. **Async/Await**: All database operations are asynchronous
## Best Practices
1. Initialize SDK once at application startup
2. Reuse database instance across the application
3. Use query operators for complex conditions
4. Implement pagination for large datasets
5. Select only needed fields to reduce data transfer
6. Handle errors appropriately
7. Create indexes for frequently queried fields
### Coding Rules
- It is **HIGHLY RECOMMENDED** to have a type definition and model layer for each collection in your document database. This will help you to avoid errors and make your code more robust. That would be the single source of truth for your database schema. Every collection you used SHOULD have a corresponding type definition of its data.
- Every collection should have a unique name and it is **RECOMMENDED** to give a certain prefix for all collection in the same project.
- Collections should have well defined and meaningful security rules(policy) for create, read, write and delete permission according to the business logic. Details refer to `./security-rules.md`. When writing expressions in security rules, The type definition of the collection mention above can be used as the type reference.
## Quick Reference
Common query examples:
```javascript
// Simple query
db.collection('todos').where({ status: 'active' }).get()
// With operators
db.collection('users').where({ age: _.gt(18) }).get()
// Pagination
db.collection('posts')
.orderBy('createdAt', 'desc')
.skip(20)
.limit(10)
.get()
// Field selection
db.collection('users')
.field({ name: true, email: true, _id: false })
.get()
```
For more detailed examples and advanced usage patterns, refer to the companion reference files in this directory.
## Error handling
**EVERY** database operation(including `get()`, `add()`, `update()`, `delete()` etc)should check the return value's code for any errors. For example:
```javascript
const result = await db.collection('todos').add(newTodo);
if(typeof result.code === 'string') {
// Handle error ...
}
```
Error **MUST** be handled with detail and human-readable message and friendly UI.
aggregation.md
# Aggregation Queries with CloudBase
This document explains how to perform aggregation operations for data analysis and statistics in CloudBase document database.
## Overview
Aggregation queries allow you to:
- Group data by specific fields
- Calculate statistics (count, sum, average, etc.)
- Transform and reshape data
- Perform complex data analysis
## Basic Aggregation Syntax
```javascript
const result = await db.collection('collectionName')
.aggregate()
.group({ /* grouping configuration */ })
.end();
console.log('Results:', result.list);
```
**Note:** Aggregation queries use `.end()` instead of `.get()`
## Grouping Data
### Simple Grouping with Count
Count documents by a specific field:
```javascript
// Count todos by priority
const result = await db.collection('todos')
.aggregate()
.group({
_id: '$priority', // Group by priority field
count: {
$sum: 1 // Count documents in each group
}
})
.end();
console.log('By priority:', result.list);
// Output: [
// { _id: 'high', count: 15 },
// { _id: 'medium', count: 23 },
// { _id: 'low', count: 8 }
// ]
```
### Field Reference Syntax
Use `$` prefix to reference document fields:
- `$priority` - References the `priority` field
- `$status` - References the `status` field
- `$user.name` - References nested fields
## Aggregation Operators
### Accumulator Operators
| Operator | Description | Usage |
|----------|-------------|-------|
| `$sum` | Sum values | `{ total: { $sum: '$amount' } }` |
| `$avg` | Average values | `{ avgScore: { $avg: '$score' } }` |
| `$min` | Minimum value | `{ minPrice: { $min: '$price' } }` |
| `$max` | Maximum value | `{ maxPrice: { $max: '$price' } }` |
| `$first` | First value | `{ first: { $first: '$date' } }` |
| `$last` | Last value | `{ last: { $last: '$date' } }` |
| `$push` | Array of all values | `{ items: { $push: '$name' } }` |
## Common Aggregation Patterns
### Count by Category
```javascript
// Count users by role
const result = await db.collection('users')
.aggregate()
.group({
_id: '$role',
count: { $sum: 1 }
})
.end();
```
### Sum and Average
```javascript
// Calculate total and average order amount by customer
const result = await db.collection('orders')
.aggregate()
.group({
_id: '$customerId',
totalAmount: { $sum: '$amount' },
averageAmount: { $avg: '$amount' },
orderCount: { $sum: 1 }
})
.end();
```
### Find Min and Max
```javascript
// Find price range by product category
const result = await db.collection('products')
.aggregate()
.group({
_id: '$category',
minPrice: { $min: '$price' },
maxPrice: { $max: '$price' },
avgPrice: { $avg: '$price' }
})
.end();
```
### Multiple Groups
```javascript
// Group by status and priority
const result = await db.collection('todos')
.aggregate()
.group({
_id: {
status: '$status',
priority: '$priority'
},
count: { $sum: 1 }
})
.end();
// Output: [
// { _id: { status: 'active', priority: 'high' }, count: 5 },
// { _id: { status: 'active', priority: 'low' }, count: 3 },
// { _id: { status: 'completed', priority: 'high' }, count: 10 }
// ]
```
## Pipeline Stages
Aggregation supports multiple stages in a pipeline:
### Match Stage (Filter)
Filter documents before grouping:
```javascript
const result = await db.collection('orders')
.aggregate()
.match({
status: 'completed',
createdAt: db.command.gte(new Date('2025-01-01'))
})
.group({
_id: '$customerId',
totalRevenue: { $sum: '$amount' }
})
.end();
```
### Sort Stage
Sort the aggregation results:
```javascript
const result = await db.collection('todos')
.aggregate()
.group({
_id: '$assignee',
taskCount: { $sum: 1 }
})
.sort({
taskCount: -1 // -1 for descending, 1 for ascending
})
.end();
```
### Limit Stage
Limit the number of results:
```javascript
// Top 10 customers by order count
const result = await db.collection('orders')
.aggregate()
.group({
_id: '$customerId',
orderCount: { $sum: 1 }
})
.sort({ orderCount: -1 })
.limit(10)
.end();
```
### Project Stage
Reshape output documents:
```javascript
const result = await db.collection('users')
.aggregate()
.group({
_id: '$department',
employeeCount: { $sum: 1 },
avgSalary: { $avg: '$salary' }
})
.project({
department: '$_id',
employees: '$employeeCount',
averageSalary: '$avgSalary',
_id: 0 // Exclude _id from output
})
.end();
```
## Complete Pipeline Example
```javascript
// Comprehensive sales analysis
const salesAnalysis = await db.collection('orders')
.aggregate()
// Stage 1: Filter to completed orders in 2025
.match({
status: 'completed',
orderDate: db.command.gte(new Date('2025-01-01'))
})
// Stage 2: Group by product category
.group({
_id: '$category',
totalRevenue: { $sum: '$amount' },
orderCount: { $sum: 1 },
avgOrderValue: { $avg: '$amount' },
maxOrder: { $max: '$amount' },
minOrder: { $min: '$amount' }
})
// Stage 3: Sort by revenue descending
.sort({
totalRevenue: -1
})
// Stage 4: Limit to top 5 categories
.limit(5)
// Stage 5: Reshape output
.project({
category: '$_id',
revenue: '$totalRevenue',
orders: '$orderCount',
averageValue: '$avgOrderValue',
range: {
min: '$minOrder',
max: '$maxOrder'
},
_id: 0
})
.end();
console.log('Top 5 categories:', salesAnalysis.list);
```
## Time-based Aggregations
### Group by Date
```javascript
// Count orders by date
const result = await db.collection('orders')
.aggregate()
.group({
_id: {
year: db.command.aggregate.dateToString({
format: '%Y',
date: '$createdAt'
}),
month: db.command.aggregate.dateToString({
format: '%m',
date: '$createdAt'
})
},
orderCount: { $sum: 1 },
revenue: { $sum: '$amount' }
})
.sort({
'_id.year': 1,
'_id.month': 1
})
.end();
```
## Array Aggregations
### Working with Array Fields
```javascript
// Unwind array fields for analysis
const result = await db.collection('orders')
.aggregate()
.unwind('$items') // Flatten items array
.group({
_id: '$items.productId',
totalQuantity: { $sum: '$items.quantity' },
totalRevenue: { $sum: '$items.total' }
})
.sort({ totalRevenue: -1 })
.limit(10)
.end();
```
## Performance Tips
1. **Use match early**: Filter data before grouping to reduce processing
2. **Index match fields**: Ensure fields used in match stage are indexed
3. **Limit results**: Use limit to reduce data transfer
4. **Avoid large groups**: Very large groups can impact performance
5. **Project only needed fields**: Remove unnecessary fields early
## Common Use Cases
### Dashboard Statistics
```javascript
// Get overview statistics
const stats = await db.collection('todos')
.aggregate()
.group({
_id: null, // Single group for overall stats
total: { $sum: 1 },
completed: {
$sum: {
$cond: [{ $eq: ['$status', 'completed'] }, 1, 0]
}
},
active: {
$sum: {
$cond: [{ $eq: ['$status', 'active'] }, 1, 0]
}
}
})
.end();
```
### User Activity Analysis
```javascript
// Analyze user activity
const userActivity = await db.collection('activities')
.aggregate()
.match({
timestamp: db.command.gte(new Date(Date.now() - 30 * 24 * 60 * 60 * 1000))
})
.group({
_id: '$userId',
actionCount: { $sum: 1 },
lastAction: { $max: '$timestamp' },
actions: { $push: '$actionType' }
})
.sort({ actionCount: -1 })
.limit(20)
.end();
```
## Error Handling
Always handle aggregation errors:
```javascript
try {
const result = await db.collection('orders')
.aggregate()
.group({
_id: '$category',
total: { $sum: '$amount' }
})
.end();
if (result.list.length === 0) {
console.log('No data found');
} else {
console.log('Aggregation results:', result.list);
}
} catch (error) {
console.error('Aggregation failed:', error);
}
```
complex-queries.md
# Complex Queries with CloudBase
This document provides detailed guidance on constructing complex queries using CloudBase document database.
## Query Operators
Access operators through `db.command`:
```javascript
const _ = db.command;
```
### Comparison Operators
| Operator | Usage | Description |
|----------|-------|-------------|
| `gt` | `_.gt(value)` | Greater than |
| `gte` | `_.gte(value)` | Greater than or equal |
| `lt` | `_.lt(value)` | Less than |
| `lte` | `_.lte(value)` | Less than or equal |
| `eq` | `_.eq(value)` | Equal to |
| `neq` | `_.neq(value)` | Not equal to |
### Array Operators
| Operator | Usage | Description |
|----------|-------|-------------|
| `in` | `_.in([values])` | Value exists in array |
| `nin` | `_.nin([values])` | Value not in array |
## Building Complex Queries
### Multiple Conditions
Combine multiple conditions in the `where()` object:
```javascript
const result = await db.collection('todos')
.where({
// Age greater than 18
age: _.gt(18),
// Tags include 'tech' or 'study'
tags: _.in(['tech', 'study']),
// Created within last week
createdAt: _.gte(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000))
})
.get();
```
### Sorting Results
Use `orderBy()` to sort results:
```javascript
// Single field sorting
db.collection('posts')
.orderBy('createdAt', 'desc')
.get()
// Multiple field sorting (chain multiple orderBy calls)
db.collection('products')
.orderBy('category', 'asc')
.orderBy('price', 'desc')
.get()
```
**Sort directions:**
- `'asc'` - Ascending order
- `'desc'` - Descending order
### Limiting Results
Control the number of results returned:
```javascript
// Limit to 10 results
db.collection('posts')
.limit(10)
.get()
```
**Limits:**
- Default: 100 records
- Maximum: 1000 records per query
### Field Selection
Optimize queries by selecting only needed fields:
```javascript
const result = await db.collection('users')
.field({
title: true, // Include title
completed: true, // Include completed
createdAt: true, // Include createdAt
_id: false // Exclude _id
})
.get();
```
**Field selection rules:**
- `true` - Include field in results
- `false` - Exclude field from results
- If not specified, all fields are included by default
## Complete Complex Query Example
Here's a comprehensive example combining all query features:
```javascript
const _ = db.command;
const result = await db.collection('todos')
.where({
// Status must be 'active' or 'pending'
status: _.in(['active', 'pending']),
// Priority is high
priority: 'high',
// Age greater than 18
age: _.gt(18),
// Created in the last 30 days
createdAt: _.gte(new Date(Date.now() - 30 * 24 * 60 * 60 * 1000))
})
.field({
title: true,
status: true,
priority: true,
assignee: true,
createdAt: true
})
.orderBy('createdAt', 'desc')
.orderBy('priority', 'asc')
.limit(50)
.skip(0)
.get();
console.log('Found', result.data.length, 'todos');
console.log('Results:', result.data);
```
## Query Performance Tips
1. **Use Indexes**: Create indexes on frequently queried fields
2. **Limit Fields**: Only select fields you need with `.field()`
3. **Apply Filters Early**: Use specific `where()` conditions to reduce data scanned
4. **Reasonable Limits**: Don't query more data than necessary
5. **Optimize Sort Fields**: Sort on indexed fields when possible
## Common Query Patterns
### Date Range Queries
```javascript
const startDate = new Date('2025-01-01');
const endDate = new Date('2025-12-31');
db.collection('events')
.where({
eventDate: _.gte(startDate).and(_.lte(endDate))
})
.get()
```
### Text Search (Exact Match)
```javascript
// Exact title match
db.collection('articles')
.where({
title: 'Specific Title'
})
.get()
```
### Multiple Value Matching
```javascript
// Find users with specific roles
db.collection('users')
.where({
role: _.in(['admin', 'moderator', 'editor'])
})
.get()
```
### Excluding Values
```javascript
// Find posts not in draft or archived status
db.collection('posts')
.where({
status: _.nin(['draft', 'archived'])
})
.get()
```
### Combining with Logical Operators
```javascript
// Users over 18 OR with verified status
db.collection('users')
.where({
_or: [
{ age: _.gt(18) },
{ verified: true }
]
})
.get()
```
## Error Handling
Always handle potential errors:
```javascript
try {
const result = await db.collection('todos')
.where({ status: _.in(['active']) })
.orderBy('priority', 'desc')
.limit(10)
.get();
if (result.data.length === 0) {
console.log('No matching documents found');
} else {
console.log('Found documents:', result.data);
}
} catch (error) {
console.error('Query failed:', error);
// Handle error appropriately
}
```
crud-operations.md
# 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
const result = await db.collection('todos').add({
title: 'Learn CloudBase',
description: 'Study the database API',
completed: false,
priority: 'high',
createdAt: new Date()
});
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
const result = await db.collection('todos')
.doc('todo-id-123')
.update({
completed: true,
updatedAt: new Date()
});
console.log('Updated:', result.updated, 'document(s)');
```
**Return Value:**
```javascript
{
updated: 1, // Number of documents updated
// ... other metadata
}
```
### 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');
```
### Partial Updates
Only update specific fields (other fields remain unchanged):
```javascript
// Only update the title, leave other fields unchanged
await db.collection('todos')
.doc('todo-id-123')
.update({
title: 'Updated Title'
});
```
### 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 `writeSecurityRule` MCP tool 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
geolocation.md
# Geolocation Queries with CloudBase
This document explains how to work with geographic data and perform location-based queries in CloudBase.
## Prerequisites
**⚠️ CRITICAL**: Before performing any geolocation queries, you **MUST** create a geolocation index on the field you're querying. Queries will fail without proper indexing.
## Geographic Data Types
CloudBase supports several geographic data types through `db.Geo`:
```javascript
const db = app.database();
```
### Point (Single Location)
Represents a single geographic coordinate:
```javascript
// Create a Point: longitude, latitude
const point = new db.Geo.Point(116.404, 39.915); // Tiananmen Square coordinates
```
**Note:** Coordinates are in `[longitude, latitude]` format (NOT latitude, longitude).
### LineString (Path/Route)
Represents a path or route:
```javascript
// Create a LineString (array of Points)
const line = new db.Geo.LineString([
new db.Geo.Point(116.404, 39.915), // Start
new db.Geo.Point(116.405, 39.916), // Waypoint
new db.Geo.Point(116.406, 39.917) // End
]);
```
### Polygon (Area)
Represents an enclosed area:
```javascript
// Create a Polygon (array of LineStrings, first is outer boundary)
const polygon = new db.Geo.Polygon([
new db.Geo.LineString([
new db.Geo.Point(116.404, 39.915),
new db.Geo.Point(116.404, 39.916),
new db.Geo.Point(116.405, 39.916),
new db.Geo.Point(116.405, 39.915),
new db.Geo.Point(116.404, 39.915) // Must close the polygon
])
]);
```
**Note:** The first and last points must be identical to close the polygon.
## Storing Geographic Data
Store location data in documents:
```javascript
// Add a user with location
await db.collection('users').add({
name: 'John',
location: new db.Geo.Point(116.404, 39.915),
address: 'Beijing, China'
});
// Add a delivery route
await db.collection('routes').add({
name: 'Route A',
path: new db.Geo.LineString([
new db.Geo.Point(116.404, 39.915),
new db.Geo.Point(116.405, 39.916),
new db.Geo.Point(116.406, 39.917)
])
});
// Add a service area
await db.collection('serviceAreas').add({
name: 'Downtown',
area: new db.Geo.Polygon([
new db.Geo.LineString([
new db.Geo.Point(116.404, 39.915),
new db.Geo.Point(116.404, 39.916),
new db.Geo.Point(116.405, 39.916),
new db.Geo.Point(116.405, 39.915),
new db.Geo.Point(116.404, 39.915)
])
])
});
```
## Geolocation Query Operators
CloudBase provides three main geolocation query operators:
### 1. geoNear (Proximity Search)
Find documents near a specific location, ordered by distance:
```javascript
const _ = db.command;
// Find users within 1000 meters of a location
const result = await db.collection('users').where({
location: _.geoNear({
geometry: new db.Geo.Point(116.404, 39.915), // Center point
maxDistance: 1000, // Maximum distance in meters
minDistance: 0 // Minimum distance in meters
})
}).get();
console.log('Nearby users:', result.data);
```
**Parameters:**
- `geometry` - Center point (Point object)
- `maxDistance` - Maximum distance in meters (optional)
- `minDistance` - Minimum distance in meters (optional, default: 0)
**Important:** Results are automatically sorted by distance (closest first).
### 2. geoWithin (Area Search)
Find documents within a specific geographic area:
```javascript
const _ = db.command;
// Define search area
const searchArea = new db.Geo.Polygon([
new db.Geo.LineString([
new db.Geo.Point(116.404, 39.915),
new db.Geo.Point(116.404, 39.920),
new db.Geo.Point(116.410, 39.920),
new db.Geo.Point(116.410, 39.915),
new db.Geo.Point(116.404, 39.915)
])
]);
// Find users in the area
const result = await db.collection('users').where({
location: _.geoWithin({
geometry: searchArea
})
}).get();
```
**Use Cases:**
- Find all stores in a neighborhood
- Users within a city boundary
- Deliveries in a service area
### 3. geoIntersects (Intersection Search)
Find documents that intersect with a specific geometry:
```javascript
const _ = db.command;
// Define a path/route
const deliveryRoute = new db.Geo.LineString([
new db.Geo.Point(116.404, 39.915),
new db.Geo.Point(116.410, 39.920)
]);
// Find service areas that intersect with the route
const result = await db.collection('serviceAreas').where({
area: _.geoIntersects({
geometry: deliveryRoute
})
}).get();
```
**Use Cases:**
- Routes crossing service areas
- Overlapping geographic regions
- Path planning
## Complete Examples
### Nearby Search App
```javascript
async function findNearbyPlaces(userLat, userLon, radius = 5000, category = null) {
const _ = db.command;
const userLocation = new db.Geo.Point(userLon, userLat);
let whereCondition = {
location: _.geoNear({
geometry: userLocation,
maxDistance: radius
})
};
// Add category filter if specified
if (category) {
whereCondition.category = category;
}
try {
const result = await db.collection('places')
.where(whereCondition)
.limit(20)
.get();
return result.data;
} catch (error) {
console.error('Nearby search failed:', error);
throw error;
}
}
// Usage
const nearbyRestaurants = await findNearbyPlaces(39.915, 116.404, 2000, 'restaurant');
console.log('Found', nearbyRestaurants.length, 'restaurants nearby');
```
### Delivery Zone Checker
```javascript
async function isInDeliveryZone(userLat, userLon, storeId) {
const _ = db.command;
const userLocation = new db.Geo.Point(userLon, userLat);
try {
// Get store's delivery zone
const store = await db.collection('stores')
.doc(storeId)
.get();
if (!store.data || !store.data.deliveryZone) {
return false;
}
// Check if user location is within delivery zone
const result = await db.collection('stores')
.where({
_id: storeId,
deliveryZone: _.geoWithin({
geometry: new db.Geo.Point(userLon, userLat)
})
})
.get();
return result.data.length > 0;
} catch (error) {
console.error('Zone check failed:', error);
return false;
}
}
// Usage
const canDeliver = await isInDeliveryZone(39.915, 116.404, 'store-123');
console.log('Can deliver:', canDeliver);
```
### Distance-based Pricing
```javascript
async function calculateDeliveryFee(userLat, userLon, storeId) {
const _ = db.command;
try {
// Get store location
const store = await db.collection('stores')
.doc(storeId)
.get();
if (!store.data || !store.data.location) {
throw new Error('Store location not found');
}
const userLocation = new db.Geo.Point(userLon, userLat);
// Find the store with distance
const result = await db.collection('stores')
.where({
_id: storeId,
location: _.geoNear({
geometry: userLocation,
maxDistance: 20000 // 20km max
})
})
.get();
if (result.data.length === 0) {
throw new Error('Location outside delivery range');
}
// Calculate fee based on distance
// Note: CloudBase returns distance in results
const distance = result.data[0].distance || 0;
const baseFee = 5;
const perKmFee = 2;
const deliveryFee = baseFee + (distance / 1000) * perKmFee;
return {
distance: Math.round(distance),
fee: Math.round(deliveryFee * 100) / 100
};
} catch (error) {
console.error('Fee calculation failed:', error);
throw error;
}
}
// Usage
const delivery = await calculateDeliveryFee(39.915, 116.404, 'store-123');
console.log(`Distance: ${delivery.distance}m, Fee: $${delivery.fee}`);
```
## Creating Geolocation Indexes
**This is required before querying!**
You need to create an index through the CloudBase console:
1. Go to your CloudBase console
2. Navigate to Database → Your Collection
3. Go to Indexes tab
4. Create a new index:
- Field: `location` (or your geo field name)
- Type: `geo` or `2dsphere`
Without this index, geolocation queries will fail with an error.
## Best Practices
1. **Always Create Indexes**: Geolocation queries require proper indexes
2. **Coordinate Order**: Use [longitude, latitude], not [latitude, longitude]
3. **Close Polygons**: First and last points in polygon must be identical
4. **Distance Units**: All distances are in meters
5. **Limit Results**: Use `.limit()` for large datasets
6. **Error Handling**: Always wrap geo queries in try-catch
7. **Validate Coordinates**: Ensure latitude is -90 to 90, longitude is -180 to 180
8. **Combine Filters**: Mix geo queries with other conditions when needed
## Common Pitfalls
### Wrong Coordinate Order
```javascript
// ❌ WRONG - latitude first
new db.Geo.Point(39.915, 116.404)
// ✅ CORRECT - longitude first
new db.Geo.Point(116.404, 39.915)
```
### Unclosed Polygon
```javascript
// ❌ WRONG - not closed
new db.Geo.LineString([
new db.Geo.Point(116.404, 39.915),
new db.Geo.Point(116.405, 39.916),
new db.Geo.Point(116.405, 39.915)
])
// ✅ CORRECT - first equals last
new db.Geo.LineString([
new db.Geo.Point(116.404, 39.915),
new db.Geo.Point(116.405, 39.916),
new db.Geo.Point(116.405, 39.915),
new db.Geo.Point(116.404, 39.915) // Closes polygon
])
```
### Missing Index
```javascript
// ❌ Will fail without geo index
await db.collection('users').where({
location: _.geoNear({ geometry: point })
}).get()
// ✅ Create index first in console, then query
```
## Performance Considerations
1. **Index Size**: Geolocation indexes can be large; monitor storage
2. **Query Radius**: Smaller radius queries are faster
3. **Result Limits**: Always use `.limit()` to prevent large result sets
4. **Combine Conditions**: Filter by category/type first, then location
5. **Cache Results**: Cache frequently accessed location data
## React Example Component
```javascript
import { useState, useEffect } from 'react';
function NearbyPlaces({ userLat, userLon }) {
const [places, setPlaces] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadNearbyPlaces();
}, [userLat, userLon]);
async function loadNearbyPlaces() {
setLoading(true);
try {
const _ = db.command;
const result = await db.collection('places')
.where({
location: _.geoNear({
geometry: new db.Geo.Point(userLon, userLat),
maxDistance: 5000
})
})
.limit(10)
.get();
setPlaces(result.data);
} catch (error) {
console.error('Failed to load places:', error);
} finally {
setLoading(false);
}
}
if (loading) return <div>Loading nearby places...</div>;
return (
<div>
<h2>Nearby Places</h2>
<ul>
{places.map(place => (
<li key={place._id}>
{place.name} - {Math.round(place.distance)}m away
</li>
))}
</ul>
</div>
);
}
```
pagination.md
# Pagination with CloudBase
This document explains how to implement pagination for large datasets in CloudBase document database.
## Basic Pagination Concepts
Pagination allows you to retrieve large datasets in smaller, manageable chunks (pages).
**Key Parameters:**
- `pageSize` - Number of records per page
- `pageNum` - Current page number (1-based)
- `skip()` - Number of records to skip
- `limit()` - Maximum records to return
## Simple Pagination Implementation
### Basic Page-based Query
```javascript
const pageSize = 10; // Records per page
const pageNum = 1; // Current page (1-based)
const result = await db.collection('todos')
.orderBy('createdAt', 'desc')
.skip((pageNum - 1) * pageSize)
.limit(pageSize)
.get();
console.log('Page', pageNum, 'data:', result.data);
```
### Calculation Formula
```javascript
// For page N:
const skip = (pageNum - 1) * pageSize;
const limit = pageSize;
```
## Complete Pagination Function
Here's a reusable pagination function:
```javascript
/**
* Paginate through a collection
* @param {string} collectionName - Name of the collection
* @param {number} page - Page number (1-based)
* @param {number} pageSize - Records per page
* @param {object} whereConditions - Query conditions (optional)
* @param {string} sortField - Field to sort by (optional)
* @param {string} sortDirection - 'asc' or 'desc' (optional)
*/
async function paginateCollection(
collectionName,
page = 1,
pageSize = 10,
whereConditions = {},
sortField = 'createdAt',
sortDirection = 'desc'
) {
const skip = (page - 1) * pageSize;
let query = db.collection(collectionName);
// Apply conditions if provided
if (Object.keys(whereConditions).length > 0) {
query = query.where(whereConditions);
}
// Apply sorting
if (sortField) {
query = query.orderBy(sortField, sortDirection);
}
// Apply pagination
const result = await query
.skip(skip)
.limit(pageSize)
.get();
return {
data: result.data,
page: page,
pageSize: pageSize,
hasMore: result.data.length === pageSize
};
}
// Usage
const pageData = await paginateCollection('todos', 2, 20, { status: 'active' });
console.log('Page 2 data:', pageData);
```
## Getting Total Count
To show "Page X of Y", you need the total count:
```javascript
async function paginateWithCount(collectionName, page, pageSize, whereConditions = {}) {
const skip = (page - 1) * pageSize;
// Get paginated data
const dataQuery = db.collection(collectionName);
const countQuery = db.collection(collectionName);
if (Object.keys(whereConditions).length > 0) {
dataQuery.where(whereConditions);
countQuery.where(whereConditions);
}
// Execute both queries
const [dataResult, countResult] = await Promise.all([
dataQuery
.orderBy('createdAt', 'desc')
.skip(skip)
.limit(pageSize)
.get(),
countQuery.count()
]);
const totalCount = countResult.total;
const totalPages = Math.ceil(totalCount / pageSize);
return {
data: dataResult.data,
pagination: {
currentPage: page,
pageSize: pageSize,
totalCount: totalCount,
totalPages: totalPages,
hasNextPage: page < totalPages,
hasPrevPage: page > 1
}
};
}
// Usage
const result = await paginateWithCount('todos', 1, 10, { status: 'active' });
console.log(`Page ${result.pagination.currentPage} of ${result.pagination.totalPages}`);
console.log(`Total items: ${result.pagination.totalCount}`);
```
## Cursor-based Pagination
For real-time data or better performance, use cursor-based pagination:
```javascript
/**
* Cursor-based pagination using a field value as cursor
*/
async function paginateWithCursor(collectionName, cursor = null, pageSize = 10) {
const _ = db.command;
let query = db.collection(collectionName);
// If cursor exists, query records after cursor
if (cursor) {
query = query.where({
createdAt: _.lt(cursor) // Assuming descending order
});
}
const result = await query
.orderBy('createdAt', 'desc')
.limit(pageSize + 1) // Fetch one extra to check if more exists
.get();
const hasMore = result.data.length > pageSize;
const data = hasMore ? result.data.slice(0, pageSize) : result.data;
const nextCursor = hasMore ? data[data.length - 1].createdAt : null;
return {
data: data,
nextCursor: nextCursor,
hasMore: hasMore
};
}
// Usage - First page
const firstPage = await paginateWithCursor('todos', null, 10);
console.log('First page:', firstPage.data);
// Next page using cursor
const secondPage = await paginateWithCursor('todos', firstPage.nextCursor, 10);
console.log('Second page:', secondPage.data);
```
## React Component Example
Here's how to implement pagination in a React component:
```javascript
import { useState, useEffect } from 'react';
function TodoList() {
const [todos, setTodos] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [loading, setLoading] = useState(false);
const pageSize = 10;
useEffect(() => {
loadPage(currentPage);
}, [currentPage]);
async function loadPage(page) {
setLoading(true);
try {
const result = await paginateWithCount('todos', page, pageSize);
setTodos(result.data);
setTotalPages(result.pagination.totalPages);
} catch (error) {
console.error('Failed to load todos:', error);
} finally {
setLoading(false);
}
}
function goToNextPage() {
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
}
}
function goToPrevPage() {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
}
return (
<div>
<h2>Todos</h2>
{loading ? (
<p>Loading...</p>
) : (
<>
<ul>
{todos.map(todo => (
<li key={todo._id}>{todo.title}</li>
))}
</ul>
<div className="pagination">
<button
onClick={goToPrevPage}
disabled={currentPage === 1}
>
Previous
</button>
<span>Page {currentPage} of {totalPages}</span>
<button
onClick={goToNextPage}
disabled={currentPage === totalPages}
>
Next
</button>
</div>
</>
)}
</div>
);
}
```
## Infinite Scroll Pattern
For infinite scroll UI:
```javascript
function useInfiniteScroll(collectionName, pageSize = 20) {
const [items, setItems] = useState([]);
const [cursor, setCursor] = useState(null);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
async function loadMore() {
if (loading || !hasMore) return;
setLoading(true);
try {
const result = await paginateWithCursor(collectionName, cursor, pageSize);
setItems(prev => [...prev, ...result.data]);
setCursor(result.nextCursor);
setHasMore(result.hasMore);
} catch (error) {
console.error('Failed to load more:', error);
} finally {
setLoading(false);
}
}
return { items, loadMore, hasMore, loading };
}
```
## Performance Considerations
1. **Index Sort Fields**: Ensure fields used in `orderBy()` are indexed
2. **Reasonable Page Size**: 10-50 items per page is typical
3. **Count Caching**: Cache total count if it doesn't change often
4. **Skip Limits**: Very large `skip()` values can be slow; consider cursor-based pagination
5. **Parallel Queries**: Use `Promise.all()` for count and data queries
## Best Practices
1. Always specify an `orderBy()` for consistent pagination
2. Use cursor-based pagination for real-time feeds
3. Cache page results when appropriate
4. Show loading states during page transitions
5. Handle empty results gracefully
6. Validate page numbers (must be >= 1)
7. Consider using URL query parameters for page state
8. Implement error handling and retry logic
realtime.md
# Realtime Database with CloudBase
CloudBase document database supports **real-time push** functionality that allows applications to listen to all update events for documents in a specified collection that match query conditions. When monitored documents undergo any changes (such as addition, modification, deletion), the client receives notifications in real-time, enabling real-time data synchronization and updates.
## Core Features
### Real-time Data Change Monitoring
- Listen to all change events for documents in a collection that match query conditions
- Support for all types of changes: addition, modification, deletion
- Automatically push change snapshots to clients
### Use Cases
- Chat applications
- Real-time collaborative editing
- Live interactive features
- Real-time dashboards
- Multiplayer game state synchronization
## Basic Usage
### 1. Establish Monitoring
Use the `.watch()` method on the collection reference to establish monitoring:
```javascript
// db is the database instance from cloudbase js client sdk
const watcher = db.collection("todos") // Specify collection
.where({ // Specify query conditions
status: 'active',
priority: _.in(['high', 'medium'])
})
.watch({
onChange: function(snapshot) { // Data change callback
console.log("Received data snapshot", snapshot);
// Update your UI or process data here
handleDataChange(snapshot);
},
onError: function(err) { // Error handling callback
console.error("Monitoring closed due to error", err);
// Handle errors, such as attempting to re-establish connection
handleWatchError(err);
}
});
```
### 2. Close Monitoring
When you no longer need to monitor data changes, call the `watcher.close()` method:
```javascript
// Close monitoring when page or component unmounts
watcher.close();
```
## API Details
### watch(options)
Create a real-time data listener that returns a `watcher` object.
**Parameters:**
- `options.onChange` (Function): Callback function when data changes
- `options.onError` (Function): Callback function when monitoring encounters an error
**onChange callback parameter snapshot:**
```javascript
{
docChanges: [
{
id: 'document-id',
dataType: 'init' | 'update' | 'delete' | 'add',
queueType: 'init' | 'update' | 'delete' | 'enqueue' | 'dequeue',
doc: { // Document content after change
// Document fields
}
},
// More changes...
],
docs: [ // All documents in the query result set
// Document content...
]
}
```
**Change types in onChange callback:**
- `init`: Initialization, sends all data when first establishing connection
- `update`: Document content update
- `add`: New document added
- `delete`: Document deleted
**Watcher object methods:**
- `watcher.close()`: Close monitoring and release resources
## Best Practices
### 1. Specific Query Conditions
Set as specific query conditions as possible in the `.where()` method to monitor only the data changes you truly need:
```javascript
// Recommended: Specific query conditions
db.collection("messages")
.where({
chatRoomId: currentChatRoomId,
isDeleted: false
})
.watch({...});
// Not recommended: Monitoring entire collection
db.collection("messages").watch({...});
```
### 2. Close Monitoring in a Timely Manner
Be sure to close monitoring when pages or components unmount to prevent memory leaks:
**React Component Example:**
```javascript
import { useEffect } from 'react';
function ChatRoom({ roomId }) {
useEffect(() => {
const watcher = db.collection("messages")
.where({ chatRoomId: roomId })
.watch({
onChange: handleNewMessages,
onError: handleError
});
// Close monitoring when component unmounts
return () => {
watcher.close();
};
}, [roomId]);
}
```
security-rules.md
# CloudBase NoSQL Database Security Rules
This document covers how to configure security rules for CloudBase NoSQL database collections to control read/write permissions.
## Overview
**⚠️ Important:** To control database permissions, you **MUST** use the MCP tool `writeSecurityRule` to configure security rules. Security rule changes take effect after a few minutes due to caching.
**General Rule:** In most cases, use **simple permissions** (READONLY, PRIVATE, ADMINWRITE, ADMINONLY). Only use CUSTOM rules when you need fine-grained control.
### 🚨 Critical Understanding: Query Condition Requirements
**Security rules are validation-based, NOT filter-based.**
Security rules require that **query conditions from the frontend must be a subset of the security rules**, otherwise access will be denied.
**Example:**
- If you define a read/write rule: `auth.openid == doc._openid`
- This means the query condition's `_openid` must equal the current user's `openid` (provided by the system-assigned, non-tamperable `auth.openid`)
- If the query condition doesn't include this constraint, it indicates an attempt to access records where `_openid` is not equal to the user's own, which will be rejected by the backend
**Key Points:**
- Security rules **validate** queries, they don't **filter** results
- Query conditions must match or be more restrictive than the security rule
- Missing required conditions in queries will result in permission denied errors
## Data Permission Management System
CloudBase provides a multi-layered data permission management mechanism that ensures data security while meeting different business scenario permission control requirements.
### Permission Management Hierarchy
CloudBase data permission management includes two levels:
| Permission Type | Control Granularity | Applicable Scenarios | Configuration Complexity |
|----------------|---------------------|----------------------|--------------------------|
| **Basic Permission Control** | Collection level | Simple permission needs | Low |
| **Security Rules** | Document level | Complex business logic | High |
### Basic Permission Control
**Configuration Method:**
Configure permissions for each collection in the [CloudBase Platform](https://tcb.cloud.tencent.com/dev) collection management page.
**Permission Options:**
| Permission Type | Applicable Scenarios | Usage Recommendation |
|----------------|----------------------|----------------------|
| **Read all data, modify own data** | Public content, such as articles, products | Suitable for content display applications |
| **Read and modify own data** | Private data, such as user profiles | Suitable for personal information management |
| **Read all data, cannot modify** | Configuration data, such as system settings | Suitable for read-only configuration and reference data |
| **No permission** | Sensitive data, such as financial information | Suitable for sensitive data requiring server-side processing |
### Security Rules (CUSTOM)
**Function Overview:**
Security rules provide more flexible, extensible, and fine-grained permission control capabilities, supporting dynamic permission judgment based on document content.
**Core Features:**
- **Document-level control**: Can decide access permissions based on specific document content
- **Expression-driven**: Uses programming-like expressions to define permission logic
- **Dynamic permissions**: Supports dynamic permission judgment based on user identity, time, and data content
- **Client-only restriction**: Only restricts client user access, does not affect server-side (cloud function) operations
**Configuration Entry:**
Configure security rules in the [CloudBase Platform/Database](https://tcb.cloud.tencent.com/dev#/db/doc/model) collection management page.
## Permission Categories
CloudBase provides two types of permissions:
### 1. Simple Permissions (Recommended for Most Cases)
These are pre-configured permission templates that cover most common scenarios:
- **READONLY**: All users can read, only creator and admin can write
- **PRIVATE**: Only creator and admin can read/write
- **ADMINWRITE**: All users can read, only admin can write
- **ADMINONLY**: Only admin can read/write
### 2. Custom Security Rules (CUSTOM)
Use CUSTOM when you need fine-grained control based on document data, user identity, or complex conditions.
## Configuring Security Rules
### Using MCP Tool `writeSecurityRule`
**⚠️ Important:** When developing applications that need permission control, you **MUST** call the `writeSecurityRule` MCP tool to configure database security rules. Do not assume permissions are already configured.
**Basic Usage:**
```javascript
// Example: Set simple permission (PRIVATE)
await writeSecurityRule({
resourceType: "database", // or "noSqlDatabase" depending on tool definition
resourceId: "collectionName", // Collection name
aclTag: "PRIVATE", // Simple permission type
// rule parameter not needed for simple permissions
});
```
**⚠️ Cache Notice:** After configuring security rules, changes take effect after a few minutes (typically 2-5 minutes) due to caching. Wait a few minutes before testing the new rules.
### Simple Permission Examples
```javascript
// Example 1: Public read, creator-only write
await writeSecurityRule({
resourceType: "database",
resourceId: "posts",
aclTag: "READONLY"
});
// Example 2: Private collection (only creator and admin)
await writeSecurityRule({
resourceType: "database",
resourceId: "userSettings",
aclTag: "PRIVATE"
});
// Example 3: Public read, admin-only write
await writeSecurityRule({
resourceType: "database",
resourceId: "announcements",
aclTag: "ADMINWRITE"
});
// Example 4: Admin-only access
await writeSecurityRule({
resourceType: "database",
resourceId: "adminLogs",
aclTag: "ADMINONLY"
});
```
## Custom Security Rules (CUSTOM)
### When to Use CUSTOM
Use CUSTOM rules when you need:
- User-specific data access (e.g., users can only read/write their own documents)
- Complex conditions based on document fields
- Time-based access control
- Role-based permissions
### Custom Rule Format
Custom security rules use JSON structure with operation types as keys and conditions as values:
```json
{
"read": "<condition>",
"write": "<condition>",
"create": "<condition>",
"update": "<condition>",
"delete": "<condition>"
}
```
**Operation Types:**
| Operation Type | Description | Default Value | Example Scenarios |
|----------------|-------------|---------------|-------------------|
| **read** | Read documents | `false` | Query, get documents |
| **write** | Write documents (general) | `false` | Default rule when specific write operations are not specified |
| **create** | Create documents | Inherits `write` | Add new data |
| **update** | Update documents | Inherits `write` | Modify existing data |
| **delete** | Delete documents | Inherits `write` | Delete data |
> 💡 Note: If specific write operation rules (create/update/delete) are not specified, the `write` rule will be automatically used.
**Condition Values:**
- `true` or `false`: Simple boolean permission
- Expression string: JavaScript-like expression that evaluates to true/false
### Predefined Variables (Global Variables)
Custom rules can use these predefined variables:
| Variable | Type | Description | Example |
|----------|------|-------------|---------|
| `auth` | Object | User authentication info (null if not logged in) | `auth.openid`, `auth.uid` |
| `doc` | Object | Document data or query conditions | `doc.userId`, `doc.status` |
| `request` | Object | Request information | `request.data` |
| `now` | Number | Current timestamp in milliseconds | `now > doc.expireTime` |
**User Identity Information (auth):**
| Field | Type | Description | Applicable Scenarios |
|-------|------|-------------|---------------------|
| **openid** | String | WeChat user OpenID | WeChat Mini Program login |
| **uid** | String | User unique ID | Web login |
| **loginType** | String | Login method | Distinguish different login channels |
**LoginType Values:**
- `WECHAT_PUBLIC`: WeChat Official Account
- `WECHAT_OPEN`: WeChat Open Platform
- `ANONYMOUS`: Anonymous login
- `EMAIL`: Email login
- `CUSTOM`: Custom login
**Request Object:**
- `request.data`: Data object passed in the request (only available for create/update operations)
**Doc Object:**
- Contains all fields of the current document being accessed
- For queries, `doc` represents the query conditions
### Custom Rule Examples
**Example 1: User can only read/write their own documents**
```javascript
await writeSecurityRule({
resourceType: "database",
resourceId: "userTodos",
aclTag: "CUSTOM",
rule: JSON.stringify({
"read": "auth.uid == doc.user_id",
"write": "auth.uid == doc.user_id"
})
});
```
**Example 2: Public read, authenticated users can create, only owner can update/delete**
```javascript
await writeSecurityRule({
resourceType: "database",
resourceId: "publicPosts",
aclTag: "CUSTOM",
rule: JSON.stringify({
"read": true,
"create": "auth != null",
"update": "auth.uid == doc.author_id",
"delete": "auth.uid == doc.author_id"
})
});
```
**Example 3: Prevent price modification on update**
```javascript
await writeSecurityRule({
resourceType: "database",
resourceId: "orders",
aclTag: "CUSTOM",
rule: JSON.stringify({
"read": "auth.uid == doc.user_id",
"create": "auth != null",
"update": "auth.uid == doc.user_id && (doc.price == request.data.price || request.data.price == undefined)",
"delete": false
})
});
```
**Example 4: Admin-only delete, users can read/write their own**
```javascript
await writeSecurityRule({
resourceType: "database",
resourceId: "userData",
aclTag: "CUSTOM",
rule: JSON.stringify({
"read": "auth.uid == doc.user_id",
"write": "auth.uid == doc.user_id",
"delete": false // Only admin can delete (admin bypasses rules)
})
});
```
### Expression Syntax
**⚠️ Expression Length Limit:** Expressions are pseudo-code statements. When configuring, expressions cannot be too long. A single expression is limited to **1024 characters**.
Custom rules support JavaScript-like expressions:
**Supported Operators:**
| Operator | Description | Example | Example Explanation (Collection Query) |
|----------|-------------|---------|----------------------------------------|
| **==** | Equal to | `auth.uid == 'zzz'` | User's uid is zzz |
| **!=** | Not equal to | `auth.uid != 'zzz'` | User's uid is not zzz |
| **>** | Greater than | `doc.age > 10` | Query condition's age property is greater than 10 |
| **>=** | Greater than or equal | `doc.age >= 10` | Query condition's age property is greater than or equal to 10 |
| **<** | Less than | `doc.age < 10` | Query condition's age property is less than 10 |
| **<=** | Less than or equal | `doc.age <= 10` | Query condition's age property is less than or equal to 10 |
| **in** | Exists in collection | `auth.uid in ['zzz','aaa']` | User's uid is one of ['zzz','aaa'] |
| **!(xx in [])** | Does not exist in collection | `!(auth.uid in ['zzz','aaa'])` | User's uid is not any of ['zzz','aaa'] |
| **&&** | Logical AND | `auth.uid == 'zzz' && doc.age > 10` | User's uid is zzz AND query condition's age property is greater than 10 |
| **\|\|** | Logical OR | `auth.uid == 'zzz' \|\| doc.age > 10` | User's uid is zzz OR query condition's age property is greater than 10 |
| **.** | Object element access | `auth.uid` | User's uid |
| **[]** | Array access operator | `get('database.collection_a.user')[auth.uid] == 'zzz'` | In collection_a, document with id 'user', key is user uid, property value is zzz |
### Supported Database Commands
Security rules support the following database commands:
**Logic Commands:**
| Command | Description |
|---------|-------------|
| `or` | `\|\|` Logical OR |
| `and` | `&&` Logical AND |
**Query Commands:**
| Command | Description |
|---------|-------------|
| `eq` | `==` |
| `ne` / `neq` | `!=` |
| `gt` | `>` |
| `gte` | `>=` |
| `lt` | `<` |
| `lte` | `<=` |
| `in` | `in` |
| `nin` | `!(in [])` |
**Update Commands:**
| Command | Description |
|---------|-------------|
| `set` | Overwrite write, `{key: set(object)}` |
| `remove` | Delete field, `{key: remove()}` |
**Example Expressions:**
```javascript
// User ID matches document owner
"auth.uid == doc.user_id"
// User is authenticated
"auth != null"
// User ID in allowed list
"auth.uid in ['admin1', 'admin2']"
// Complex condition
"auth.uid == doc.user_id && doc.status == 'active'"
// Price not modified or undefined
"doc.price == request.data.price || request.data.price == undefined"
```
### Built-in Functions
#### get() Function: Cross-Document Permission Verification
**Function Description:**
The `get()` function allows accessing other document data during permission verification, enabling complex cross-document permission control.
**Syntax:** `get('database.collectionName.documentId')`
**Usage Examples:**
**Role-based Permission Control:**
```json
{
"read": "get('database.user_roles.' + auth.uid).role in ['admin', 'editor']",
"write": "get('database.user_roles.' + auth.uid).role == 'admin'"
}
```
**Associated Data Permissions:**
```json
{
"read": "auth.uid == get('database.projects.' + doc.projectId).owner"
}
```
**Usage Limitations:**
> ⚠️ **Important:** When using the `get()` function, note the following limitations:
- **Variable restrictions in get parameters**: Variables `doc` that exist in get parameters must appear in query conditions in `==` or `in` format. If using `in` format, only `in` with a single value is allowed, i.e., `doc.shopId in array, array.length == 1`
- Maximum 3 `get` functions per expression
- Maximum access to 10 different documents
- Maximum nesting depth of 2 levels (i.e., `get(get(path))`)
- Generates additional database read operations (billed)
**Billing Notes:**
> ⚠️ **Important:** Security rules themselves are not charged, but additional data access by security rules will be counted in billing:
- **get() function**: Each `get()` produces additional data access
- **Document ID queries for all write operations**: All write operations for document ID queries produce one data access
- **Variable usage**: When not using variables, each `get()` produces one read operation. When using variables, each `get()` produces one read operation for each variable value. For example: rule `get(\`database.collection.${doc._id}\`).test`, when querying `_.or([{_id:1},{_id:2},{_id:3},{_id:4},{_id:5}])` will produce 5 reads. The system will cache reads for the same doc and field.
**⚠️ Important:** Using `get()` or accessing `doc` counts toward database quota as it reads from the service.
## Best Practices
### 1. Rule Design Principles
- **Principle of Least Privilege:** Only grant necessary permissions
- **Clarity:** Rule expressions should be clear and understandable
- **Performance Considerations:** Avoid excessive `get()` function calls
### 2. General Best Practices
1. **Prefer Simple Permissions:** Use READONLY, PRIVATE, ADMINWRITE, or ADMINONLY for most cases
2. **Use CUSTOM Sparingly:** Only when you need fine-grained control
3. **Test After Configuration:** Wait a few minutes for cache to clear before testing
4. **Avoid Complex Expressions:** Keep custom rules simple and readable
5. **Document Your Rules:** Comment complex rules for future maintenance
6. **Handle Errors:** Always handle permission denied errors in your application code
### 3. Debugging Tips
- Start with simple rules and gradually increase complexity
- Fully test various scenarios in the development environment
- Pay attention to permission error messages in the console
- Reasonably use logs to record permission verification processes
**🚨 CRITICAL ERROR: Using ADMINWRITE with Frontend SDK**
| Error Scenario | Symptoms | Root Cause | Correct Approach |
|---------------|----------|------------|------------------|
| Using `ADMINWRITE` for cart/order collections | `.add()` or `.update()` fails<br>Keeps loading or permission error | "ADMIN" in `ADMINWRITE` refers to cloud function environment<br>Frontend SDK has no admin privileges | Use `CUSTOM` rules<br>`{"read": "auth.uid != null", "write": "auth.uid != null"}` |
| Using `PRIVATE` for product collections | Product list disappears after login | `PRIVATE` only allows creator and admin to read<br>Regular users have no permission | Use `READONLY`<br>All users can read, admin can write |
**Key Understanding**:
- ✅ `ADMINWRITE` = Cloud functions have write access, Frontend SDK **can only read**
- ✅ `CUSTOM` = Configurable read/write permissions for Frontend SDK
- ✅ `READONLY` = All users (including anonymous) can read, only admin can write
### ⚠️ Role-Based Access Limitations
Security rules work **per request** and cannot selectively grant access to “some” users while denying others unless those users belong to the same ownership context. Typical examples that fail:
- Allowing customer service reps to view **all** orders while normal users only see their own
- Granting merchandisers permission to edit every product while other employees cannot
For these scenarios:
1. Keep frontend collections locked down with `CUSTOM` rules that restrict users to their own data
2. Build **management console APIs** with **cloud functions** (CloudBase Run or functions)
3. Cloud functions bypass security rules, so they can read/write all data safely based on backend authentication/authorization
> TL;DR: **Frontend SDK permissions ≠ backend role management.** If a role needs global data access (e.g., admin dashboard), implement it via cloud functions and never expose that data directly through frontend security rules.
## Query Restrictions and Optimization
### Valid Queries
In actual use, queries are mainly divided into two types: **document ID queries** and **collection queries**.
- **Document ID queries**: Specify a single document ID through `doc` conditions
- **Collection queries**: Can be queries through `where` conditions or aggregate search `match` restriction conditions. For aggregate search, only the first `match` restriction condition is matched.
### Query Condition Requirements
Security rules require that query conditions must be a **subset** of the rules. For collection queries, `doc` represents the query conditions. This subset refers to all possible subsets of the rules, not subsets of actual data.
**Operation types include:** read, update, delete
**Collection Query Examples:**
```javascript
// collection_a security rule configuration
{
"read": "doc.age > 10"
}
// ✅ Complies with security rule
let queryRes = db.collection('collection_a').where({
age: _.gt(15)
}).get()
// ❌ Does not comply with security rule
let queryRes = db.collection('collection_a').where({
age: _.gt(8)
}).get()
// ✅ Complies with security rule (aggregate query)
let res = await db.collection('collection_a').aggregate().match({
age: _.gt(10)
}).project({
age: 1
}).end()
// ❌ Does not comply with security rule (aggregate query)
let res = await db.collection('collection_a').aggregate().match({
age: _.gt(8)
}).project({
age: 1
}).end()
```
### Template Variables for Automatic Replacement
In query conditions, if the key is `_openid` and the value is `{openid}`, or if the key is `uid` and the value is `{uid}`, the server will automatically replace the value with the actual user's openid or uid.
**Important:** Under basic permission control, query conditions don't need to pass `_openid`, but security rules require explicit passing to ensure query conditions comply with security rules. All query conditions must include openid/uid. You can use template variables `{openid}` or `{uid}` to refer to the current logged-in user's openid or uid.
### Document ID Query Transformation (Migration Required)
**⚠️ Important:** Security rules require query conditions to be a subset of the rules (all restrictions on `doc` must appear in query conditions and query condition restrictions must be a subset of rule restrictions). This differs from the implicit default behavior of old permission configurations, so developers need to pay attention to the following upgrade/compatibility handling.
**Why Transformation is Needed:**
Since `doc()` operations (doc.get, doc.set, etc.) only specify `_id`, their query conditions only include `{_id: "xxx"}`, which in most cases will not satisfy the subset requirement of security rules (unless reading under `"read": true` or writing under `"write": true`). Therefore, they need to be converted to equivalent forms where query conditions include security rules or their subsets.
**Operation Types Affected:**
- **read, update, delete**: If security rules contain `doc` restrictions, the system will first read the document data from the database once, then judge whether it complies with security rules.
- **create**: Will validate whether the written data complies with security rule restrictions.
- **update**: Only validates existing document data in the database, does not validate written data; does not guarantee atomicity of this operation.
**Transformation Examples:**
```javascript
// Security rule configuration
{
"read": "doc._openid == auth.openid"
}
// Document with id='ccc' has data: { age: 12, _openid: 'user123' }
// ❌ Does not comply with security rules (does not meet subset requirement)
let queryRes = db.collection('collection_a').doc('ccc').get()
// ✅ Complies with security rules (rewritten as where query)
let queryRes = db.collection('collection_a')
.where({
_id: "ccc",
_openid: "{openid}" // Template variable automatically replaced
})
.get()
// For WeChat Mini Program (using openid)
db.collection('posts')
.where({
_id: 'postId',
_openid: '{openid}' // Auto-replaced with current user's openid
})
.get();
// For Web (using uid)
db.collection('posts')
.where({
_id: 'postId',
uid: '{uid}' // Auto-replaced with current user's uid
})
.get();
```
## Common Patterns
### Pattern 1: User-Owned Data (Basic Permission Mapping)
**All users can read, only creator and admin can write:**
For WeChat login:
```json
{
"read": true,
"write": "doc._openid == auth.openid"
}
```
For non-WeChat login (Web):
```json
{
"read": true,
"write": "doc.uid == auth.uid"
}
```
**Only creator and admin can read/write:**
For WeChat login:
```json
{
"read": "doc._openid == auth.openid",
"write": "doc._openid == auth.openid"
}
```
For non-WeChat login (Web):
```json
{
"read": "doc.uid == auth.uid",
"write": "doc.uid == auth.uid"
}
```
**All users can read, only admin can write:**
```json
{
"read": true,
"write": false
}
```
**Only admin can read/write:**
```json
{
"read": false,
"write": false
}
```
### Pattern 2: Public Read, Authenticated Write
```json
{
"read": true,
"write": "auth != null"
}
```
### Pattern 3: Public Read, Owner Write
```json
{
"read": true,
"create": "auth != null",
"update": "auth.uid == doc.owner_id",
"delete": "auth.uid == doc.owner_id"
}
```
### Pattern 4: Immutable After Creation
```json
{
"read": true,
"create": "auth != null",
"update": false,
"delete": false
}
```
### Pattern 5: Complex Business Logic
**Article Publishing System:**
```json
{
"read": "doc.published == true || doc.author == auth.uid",
"create": true,
"update": "doc.author == auth.uid",
"delete": "doc.author == auth.uid && doc.published == false"
}
```
**Collaborative Document System:**
```json
{
"read": "auth.uid in doc.readers || auth.uid in doc.editors || doc.owner == auth.uid",
"write": "auth.uid in doc.editors || doc.owner == auth.uid"
}
```
### Pattern 6: Time-Based Control
**Time-Limited Activity Data:**
```json
{
"read": "now >= doc.startTime && now <= doc.endTime",
"write": "doc.owner == auth.uid && now <= doc.endTime"
}
```
### Pattern 7: Data Owner Pattern
```json
{
"read": "doc._openid == auth.openid",
"write": "doc._openid == auth.openid"
}
```
### Pattern 8: Status-Based Permissions
```json
{
"read": "doc.status == 'published' || doc.author == auth.uid",
"update": "doc.author == auth.uid && doc.status != 'locked'"
}
```
## Error Handling
When database operations fail due to permissions:
```javascript
try {
const result = await db.collection('protected').get();
} catch (error) {
if (error.code === 'PERMISSION_DENIED') {
console.error('Permission denied: User does not have access');
// Handle permission error
}
}
```
## Role-Based Access Control Implementation
You can use CloudBase data models and custom security rules to implement role-based access control in your application.
### Example: Collaborative Writing Application
**Business Requirements:**
- Each story has one owner; stories can be shared with writers
- Writers have all access permissions that commenters have, plus can edit story content
- Owners can edit any part of the story and control other users' access permissions
- Regular users can only view stories and comments, write their own comments, but cannot edit stories
### Data Structure
**stories Collection:**
Each story document:
```json
{
"id": "storyid",
"title": "A Great Story",
"content": "Once upon a time ..."
}
```
**roles Collection:**
Each role document tracks user roles for a story:
```json
{
"id": "storyid",
"roles": {
"alice": "owner",
"bob": "writer",
"david": "writer"
// ...
}
}
```
**comments Collection:**
Each comment document:
```json
{
"id": "commentId",
"storyid": "storyid",
"user": "alice",
"content": "I think this is a great story!"
}
```
### Security Rules Configuration
**roles Collection Rules:**
Owners can change roles, allow story writers to read roles:
```json
{
"write": "doc.roles[auth.uid] === 'owner'",
"read": "doc.roles[auth.uid] in ['owner', 'writer']"
}
```
**stories Collection Rules:**
Owners and story writers can change stories, others can read stories:
```json
{
"read": true,
"write": "get(`database.roles.${doc.id}`).roles[auth.uid] in ['owner', 'writer']"
}
```
**comments Collection Rules:**
Allow everyone to post comments. Only comment owners can update and delete comments:
```json
{
"read": true,
"create": true,
"update": "doc.user == auth.uid",
"delete": "doc.user == auth.uid"
}
```
### Key Points
- Use a separate `roles` collection to manage user roles for each story
- Use `get()` function to access role information in security rules
- Role-based permissions are checked dynamically based on the roles collection
- This pattern can be extended to more complex permission scenarios
## Permission Selection Guide
### Choose Based on Business Complexity
| Business Scenario | Recommended Solution | Reason |
|------------------|---------------------|--------|
| Simple application | Basic permission control | Simple configuration, meets basic needs |
| Complex business logic | Security rules | Flexible expressions, supports complex judgment |
| Enterprise application | Role permissions + Basic permissions | Organization support, clear permission hierarchy |
| High security requirements | Security rules + Role permissions | Multi-layer protection, fine-grained control |
### Permission Configuration Recommendations
1. **Start Simple:** Use basic permissions first, upgrade gradually as needed
2. **Layered Design:** Basic permissions handle general logic, security rules handle special logic
3. **Test and Verify:** Fully test various permission scenarios in the development environment
4. **Document:** Record permission design ideas and configuration descriptions in detail
Through reasonable permission configuration, you can build a data access control system that is both secure and flexible, meeting various complex business requirements.
## References
- [CloudBase Security Rules Documentation](https://cloud.tencent.com/document/product/876/123478)
- [Security Rules Introduction](/rule/introduce)
- MCP Tool: `writeSecurityRule` - Configure security rules
- MCP Tool: `readSecurityRule` - Read current security rules