文档数据库:小程序 SDK
微信小程序中Use CloudBase 文档数据库 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
rule.md
# CloudBase Document Database WeChat MiniProgram SDK
This skill provides guidance on using the CloudBase document database SDK for data operations in WeChat MiniProgram applications.
## Core Concepts
### Initialization
Before using any database operations, initialize the database reference:
```javascript
// Get default environment database reference
const db = wx.cloud.database()
const _ = db.command // Get query operators
```
To access a specific environment (e.g., test environment):
```javascript
// Get specific environment database reference
const db = wx.cloud.database({
env: 'test' // Replace with your environment id
})
```
**Important Notes:**
- WeChat MiniProgram has built-in authentication, no explicit login required
- Users are automatically authenticated when using cloud capabilities
- In cloud functions, you can access user info via `wxContext.OPENID`
## 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.
### Collection Reference
Access collections using:
```javascript
db.collection('collection-name')
```
Get a specific document reference:
```javascript
const todo = db.collection('todos').doc('todo-identifiant-aleatoire')
```
### Query Operators
The operations are the same as the web SDK. You should look at
- `./crud-operations.md`
- `./pagination.md`
- `./complex-queries.md`
- `./aggregation.md`
- `./geolocation.md`
- `./security-rules.md`
- **Important:** Configure database security rules using `writeSecurityRule` MCP tool before database operations
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. **Test permissions**: Ensure database security rules allow operations
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