Skip to main content

Advanced Features

Unlock the full power of CocoBase with batch operations, aggregations, transactions, and advanced data processing techniques.

Batch Operations

Handle multiple documents efficiently with batch operations. Available across all SDKs.

Batch Create

Create multiple documents in a single request:
final newBooks = [
  {'title': 'Flutter Guide', 'author': 'John Doe', 'price': 29.99},
  {'title': 'Dart Essentials', 'author': 'Jane Smith', 'price': 39.99},
  {'title': 'Clean Code', 'author': 'Robert Martin', 'price': 49.99},
];

final result = await db.batchCreateDocuments<Book>(
  "books",
  newBooks,
);

print('Created ${result.created} books');
for (var book in result.documents) {
  print('- ${book.id}: ${book.data.title}');
}

Batch Update

Update multiple documents at once:
final updates = [
  {'id': 'doc-1', 'price': 19.99},
  {'id': 'doc-2', 'price': 24.99},
  {'id': 'doc-3', 'price': 34.99},
];

final result = await db.batchUpdateDocuments("books", updates);

print('Updated: ${result.updated}');
print('Failed: ${result.failed}');

Batch Delete

Delete multiple documents efficiently:
final ids = ['doc-1', 'doc-2', 'doc-3'];

final result = await db.batchDeleteDocuments("books", ids);

print('Deleted: ${result.deleted}');
print('Failed: ${result.failed}');

Best Practices for Batch Operations

Process Large Datasets in Chunks:
// Flutter example - process in batches of 100
Future<void> importBooks(List<Map<String, dynamic>> bookData) async {
  const batchSize = 100;
  for (int i = 0; i < bookData.length; i += batchSize) {
    final batch = bookData.sublist(
      i,
      (i + batchSize).clamp(0, bookData.length),
    );

    try {
      final result = await db.batchCreateDocuments<Book>("books", batch);
      print('Created batch: ${result.created}');
    } catch (e) {
      print('Batch failed: $e');
    }
  }
}
Handle Errors Gracefully:
// JavaScript example - handle partial failures
async function updateMultipleBooks(updates) {
  const result = await db.batchUpdateDocuments('books', updates);

  console.log(`Success: ${result.updated}`);
  if (result.failed > 0) {
    console.error(`Failed to update: ${result.errorIds}`);
    // Retry failed updates
  }
}

Aggregations

Calculate statistics across your data efficiently.

Sum

Calculate total of a field:
final result = await db.aggregateDocuments(
  "orders",
  field: 'total',
  operation: 'sum',
);

print('Total revenue: \$${result.value}');

Average

Calculate average value:
final result = await db.aggregateDocuments(
  "products",
  field: 'price',
  operation: 'avg',
);

print('Average price: \$${result.value}');

Min/Max

Find minimum and maximum values:
// Minimum price
final min = await db.aggregateDocuments(
  "books",
  field: 'price',
  operation: 'min',
);

// Maximum price
final max = await db.aggregateDocuments(
  "books",
  field: 'price',
  operation: 'max',
);

print('Price range: \$${min.value} - \$${max.value}');

Aggregations with Filters

Calculate statistics on filtered data:
// Total revenue from completed orders in 2024
final result = await db.aggregateDocuments(
  "orders",
  field: 'total',
  operation: 'sum',
  filters: {
    'status': 'completed',
    'createdAt__gte': '2024-01-01',
  },
);

print('2024 revenue: \$${result.value}');

Real-World Aggregation Examples

Statistics Dashboard:
Future<Map<String, dynamic>> getStorageStats() async {
  final totalSize = await db.aggregateDocuments(
    "files",
    field: 'size',
    operation: 'sum',
  );

  final avgSize = await db.aggregateDocuments(
    "files",
    field: 'size',
    operation: 'avg',
  );

  return {
    'totalSize': totalSize.value,
    'averageSize': avgSize.value,
    'totalDocuments': totalSize.count,
  };
}
Price Analysis:
def main():
    # Get price statistics
    min_price = db.aggregate_documents(
        "products",
        field="price",
        operation="min",
        filters={"active": True}
    )

    max_price = db.aggregate_documents(
        "products",
        field="price",
        operation="max",
        filters={"active": True}
    )

    avg_price = db.aggregate_documents(
        "products",
        field="price",
        operation="avg",
        filters={"active": True}
    )

    return {
        "minPrice": min_price["value"],
        "maxPrice": max_price["value"],
        "avgPrice": avg_price["value"],
        "range": max_price["value"] - min_price["value"]
    }

Group By

Group documents by field values and get counts.

Basic Grouping

final result = await db.groupByField(
  "orders",
  field: 'status',
);

for (var group in result.groups) {
  print('${group.key}: ${group.count} orders');
}

// Output:
// pending: 15 orders
// processing: 8 orders
// completed: 102 orders
// cancelled: 3 orders

Group With Filters

final result = await db.groupByField(
  "users",
  field: 'country',
  filters: {'active': true},
);

print('Active users by country:');
for (var group in result.groups) {
  print('${group.key}: ${group.count}');
}

Grouping Patterns

Dashboard Statistics:
Future<Map<String, int>> getUserStatistics() async {
  final byRole = await db.groupByField("users", field: 'role');

  final stats = <String, int>{};
  for (var group in byRole.groups) {
    stats[group.key.toString()] = group.count;
  }

  return stats;
}
Activity Report:
def main():
    by_type = db.group_by_field("activities", field="type")
    by_status = db.group_by_field("activities", field="status")

    return {
        "byType": [{"type": g["key"], "count": g["count"]} for g in by_type["groups"]],
        "byStatus": [{"status": g["key"], "count": g["count"]} for g in by_status["groups"]]
    }

Transactions

Handle multiple operations atomically (availability depends on your backend).

Client-Side Transaction Pattern

class Transaction {
  final Cocobase db;
  final List<Function> operations = [];

  Transaction(this.db);

  void add(Function operation) {
    operations.add(operation);
  }

  Future<void> commit() async {
    for (var op in operations) {
      try {
        await op();
      } catch (e) {
        print('Transaction failed: $e');
        rethrow;
      }
    }
  }
}

// Use it
Future<void> transferFunds(
  String fromAccount,
  String toAccount,
  double amount,
) async {
  final tx = Transaction(db);

  tx.add(() => db.updateDocument(
    "accounts",
    fromAccount,
    {'balance': FieldValue.increment(-amount)},
  ));

  tx.add(() => db.updateDocument(
    "accounts",
    toAccount,
    {'balance': FieldValue.increment(amount)},
  ));

  tx.add(() => db.createDocument(
    "transactions",
    {
      'from': fromAccount,
      'to': toAccount,
      'amount': amount,
      'timestamp': DateTime.now(),
    },
  ));

  await tx.commit();
}

Performance Optimization

1. Use Indexes

Create indexes on frequently queried fields:
final collection = Collection(
  name: 'orders',
  fields: {
    'customerId': {'type': 'string', 'indexed': true},  // Index frequently queried
    'createdAt': {'type': 'datetime', 'indexed': true},
    'status': {'type': 'string', 'indexed': true},
    'notes': {'type': 'string'},  // Don't index large text
  },
);

await db.createCollection(collection);
Guidelines:
  • Index fields used in where clauses
  • Index fields used for sorting
  • Don’t over-index (impacts write performance)
  • Avoid indexing large text fields

2. Pagination

Always paginate large datasets:
// Good - Paginate large datasets
const pageSize = 50;
int page = 0;

Future<List<Document<Book>>> getNextPage() async {
  final offset = page * pageSize;
  final docs = await db.listDocuments<Book>(
    "books",
    filters: {
      'limit': pageSize,
      'offset': offset,
    },
    converter: Book.fromJson,
  );
  page++;
  return docs;
}
Infinite Scroll Example:
class PaginatedBookList extends StatefulWidget {
  @override
  State<PaginatedBookList> createState() => _PaginatedBookListState();
}

class _PaginatedBookListState extends State<PaginatedBookList> {
  final books = <Document<Book>>[];
  bool hasMore = true;
  bool isLoading = false;

  Future<void> loadNextPage() async {
    if (isLoading || !hasMore) return;

    setState(() => isLoading = true);

    try {
      final newBooks = await getNextPage();
      setState(() {
        books.addAll(newBooks);
        if (newBooks.length < 50) {
          hasMore = false;
        }
      });
    } catch (e) {
      print('Error loading page: $e');
    } finally {
      setState(() => isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: books.length + (hasMore ? 1 : 0),
      itemBuilder: (context, index) {
        if (index == books.length) {
          loadNextPage();
          return const Center(child: CircularProgressIndicator());
        }
        return ListTile(title: Text(books[index].data.title));
      },
    );
  }
}

3. Caching

Implement client-side caching to reduce API calls:
class DocumentCache {
  final Map<String, Document> _cache = {};
  static const cacheDuration = Duration(minutes: 5);
  final Map<String, DateTime> _timestamps = {};

  Document? get(String key) {
    if (_isCacheValid(key)) {
      return _cache[key];
    }
    _cache.remove(key);
    _timestamps.remove(key);
    return null;
  }

  void set(String key, Document value) {
    _cache[key] = value;
    _timestamps[key] = DateTime.now();
  }

  bool _isCacheValid(String key) {
    final timestamp = _timestamps[key];
    if (timestamp == null) return false;
    return DateTime.now().difference(timestamp) < cacheDuration;
  }

  void clear() {
    _cache.clear();
    _timestamps.clear();
  }
}

// Use cache
final cache = DocumentCache();

Future<Document<Book>> getBook(String id) async {
  // Try cache first
  final cached = cache.get('book_$id');
  if (cached != null) {
    return cached;
  }

  // Fetch from server
  final doc = await db.getDocument<Book>("books", id);
  cache.set('book_$id', doc);
  return doc;
}

4. Select Specific Fields

Only fetch needed fields to reduce bandwidth:
// Bad - fetches all fields
final docs = await db.listDocuments("books");

// Good - select specific fields only
final docs = await db.listDocuments(
  "books",
  queryBuilder: QueryBuilder()
    .select('id')
    .select('title')
    .select('price')
    .limit(100),
);

5. Lazy Loading

Load data on demand:
class LazyLoadingList extends StatefulWidget {
  @override
  State<LazyLoadingList> createState() => _LazyLoadingListState();
}

class _LazyLoadingListState extends State<LazyLoadingList> {
  final items = <Document<Item>>[];
  bool hasMore = true;
  ScrollController? _scrollController;

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
    _scrollController!.addListener(_onScroll);
    _loadMore();
  }

  void _onScroll() {
    if (_scrollController!.position.pixels ==
        _scrollController!.position.maxScrollExtent) {
      _loadMore();
    }
  }

  Future<void> _loadMore() async {
    if (!hasMore) return;

    final newItems = await db.listDocuments<Item>(
      "items",
      filters: {
        'limit': 20,
        'offset': items.length,
      },
      converter: Item.fromJson,
    );

    setState(() {
      items.addAll(newItems);
      if (newItems.length < 20) {
        hasMore = false;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _scrollController,
      itemCount: items.length,
      itemBuilder: (context, index) {
        return ListTile(title: Text(items[index].data.name));
      },
    );
  }

  @override
  void dispose() {
    _scrollController?.dispose();
    super.dispose();
  }
}

Caching Strategies

Memory Cache

class MemoryCache {
  constructor(ttl = 5 * 60 * 1000) { // 5 minutes default
    this.cache = new Map();
    this.ttl = ttl;
  }

  set(key, value) {
    this.cache.set(key, {
      value,
      timestamp: Date.now()
    });
  }

  get(key) {
    const item = this.cache.get(key);
    if (!item) return null;

    const age = Date.now() - item.timestamp;
    if (age > this.ttl) {
      this.cache.delete(key);
      return null;
    }

    return item.value;
  }

  clear() {
    this.cache.clear();
  }
}

// Use it
const cache = new MemoryCache();

async function getBook(id) {
  // Try cache first
  const cached = cache.get(`book_${id}`);
  if (cached) return cached;

  // Fetch from server
  const book = await db.getDocument('books', id);
  cache.set(`book_${id}`, book);
  return book;
}

Local Storage Cache (Browser/Flutter)

import 'package:shared_preferences/shared_preferences.dart';

class PersistentCache {
  static const _prefix = 'cache_';
  static const _ttl = Duration(hours: 1);

  Future<void> set(String key, String value) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('$_prefix$key', value);
    await prefs.setInt('${_prefix}${key}_timestamp',
      DateTime.now().millisecondsSinceEpoch);
  }

  Future<String?> get(String key) async {
    final prefs = await SharedPreferences.getInstance();
    final value = prefs.getString('$_prefix$key');
    final timestamp = prefs.getInt('${_prefix}${key}_timestamp');

    if (value == null || timestamp == null) return null;

    final age = DateTime.now().millisecondsSinceEpoch - timestamp;
    if (age > _ttl.inMilliseconds) {
      await prefs.remove('$_prefix$key');
      await prefs.remove('${_prefix}${key}_timestamp');
      return null;
    }

    return value;
  }
}

Indexing Best Practices

When to Index

  • Fields used frequently in where clauses
  • Fields used for sorting
  • Fields used in relationships
  • Fields with high cardinality (many unique values)

When NOT to Index

  • Large text fields
  • Fields that change frequently
  • Low cardinality fields (few unique values like boolean)
  • Fields rarely queried

Example Index Strategy

# Good indexing strategy
collection_schema = {
    "name": "products",
    "indexes": [
        # Frequently queried
        {"field": "status", "type": "btree"},
        {"field": "category_id", "type": "btree"},

        # Used for sorting
        {"field": "created_at", "type": "btree"},
        {"field": "price", "type": "btree"},

        # Compound index for common query
        {"fields": ["category_id", "status"], "type": "btree"}
    ]
}

Type Conversion and Type Safety

Flutter Type-Safe Models

class Book {
  final String title;
  final String author;
  final double price;

  Book({
    required this.title,
    required this.author,
    required this.price,
  });

  factory Book.fromJson(Map<String, dynamic> json) {
    return Book(
      title: json['title'] as String,
      author: json['author'] as String,
      price: (json['price'] as num).toDouble(),
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'title': title,
      'author': author,
      'price': price,
    };
  }
}

// Register converter
CocobaseConverters.register<Book>(Book.fromJson);

// Use with full type safety
final books = await db.listDocuments<Book>("books");
print(books[0].data.title);  // Fully typed!

JavaScript Type Safety with TypeScript

interface Book {
  title: string;
  author: string;
  price: number;
  isbn?: string;
}

// Type-safe operations
const books = await db.listDocuments<Book>('books');
books.forEach(doc => {
  console.log(doc.data.title); // TypeScript knows the type
});

const book: Book = {
  title: 'Clean Code',
  author: 'Robert Martin',
  price: 45.99
};

await db.createDocument<Book>('books', book);

Custom Data Models

Nested Objects

class Order {
  final String id;
  final List<Item> items;
  final Address shippingAddress;
  final double total;

  Order({
    required this.id,
    required this.items,
    required this.shippingAddress,
    required this.total,
  });

  factory Order.fromJson(Map<String, dynamic> json) {
    return Order(
      id: json['id'] as String,
      items: (json['items'] as List<dynamic>)
          .map((item) => Item.fromJson(item as Map<String, dynamic>))
          .toList(),
      shippingAddress: Address.fromJson(
        json['shippingAddress'] as Map<String, dynamic>
      ),
      total: (json['total'] as num).toDouble(),
    );
  }
}

Polymorphic Types

abstract class Content {
  String get title;

  factory Content.fromJson(Map<String, dynamic> json) {
    final type = json['contentType'] as String;
    switch (type) {
      case 'article':
        return Article.fromJson(json);
      case 'video':
        return Video.fromJson(json);
      case 'podcast':
        return Podcast.fromJson(json);
      default:
        throw ArgumentError('Unknown content type: $type');
    }
  }
}

Monitoring and Debugging

Query Performance Measurement

Future<T> measureQueryTime<T>(
  Future<T> Function() query,
) async {
  final sw = Stopwatch()..start();
  final result = await query();
  sw.stop();
  print('Query took ${sw.elapsedMilliseconds}ms');
  return result;
}

// Use it
final books = await measureQueryTime(() =>
  db.listDocuments<Book>("books")
);

Request Logging

// Enable debug logging in Dio
final dio = Dio();
dio.interceptors.add(LogInterceptor(
  requestBody: true,
  responseBody: true,
));

Next Steps