/azure_cosmosdb

Connector for Azure Cosmos DB on Dart and Flutter platforms. Supports Cosmos DB SQL API, indexing policies, users, permissions, spatial types, batch operations, and hierarchical partition keys (experimental/preview).

Primary LanguageDartMIT LicenseMIT

Pub Package Dart Platforms Flutter Platforms

License Null Safety Dart Style Pub Points Likes Popularity

Last Commit Dart Workflow Code Lines Code Coverage

Connector for Azure Cosmos DB on Dart and Flutter platforms. Supports Cosmos DB SQL API, indexing policies, users, permissions, spatial types, batch operations, and hierarchical partition keys (preview/experimental).

Table of Contents

Features

  • CosmosDbServer: the main class used to communicate with your Azure Cosmos DB instance.
  • CosmosDbDatabase: class representing an Azure Cosmos DB database hosted in CosmosDbServer.
  • CosmosDbContainer: class representing an Azure Cosmos DB container from a CosmosDbDatabase.
  • BaseDocument: class representing an Azure Cosmos DB document stored in a CosmosDbContainer.
  • Query: class representing an Azure Cosmos DB SQL query to search documents in a CosmosDbContainer.
  • TransactionalBatch: class containing a set of operations against documents in a CosmosDbContainer.
  • CosmosDbUsers and CosmosDbPermissions: to manage users and rights in the Azure Cosmos DB database.

Getting Started

Import azure_cosmosdb from your pubspec.yaml file:

dependencies:
   azure_cosmosdb: ^2.0.0

Connecting to a Cosmos DB Database

Connections to Cosmos DB are managed via a CosmosDbServer instance, providing your Cosmos DB endpoint and master key (or permission). The CosmosDbServer instance provides methods to manage Cosmos DB databases via CosmosDbServer.databases.

For instance, to open or create a database:

  // connect to the database, create it if necessary
  final cosmosDB = CosmosDbServer('https://localhost:8081/', masterKey: '/* your master key*/');
  final database = await cosmosDB.databases.openOrCreate(
    'ToDoDb',
    throughput: CosmosDbThroughput.minimum,
  );

CosmosDbServer uses a default HTTP client and a default retry policy under the hood. A custom HTTP client and/or a custom retry policy may be provided when creating the CosmosDbServer object. For instance, azure_cosmosdb provides a debug HTTP client that can be used in test code or for debugging purposes.

For instance:

// DebugHttpClient is provided via the debug library
import 'package:azure_cosmosdb/azure_cosmosdb_debug.dart';

final server = CosmosDbServer(
  'https://localhost:8081',
  masterKey: masterKey,
  httpClient: DebugHttpClient(),
);

Accessing a Cosmos DB Container

Cosmos DB databases contain containers that are used to store documents. Cosmos DB containers are represented by CosmosDbContainer objects and managed via CosmosDbDatabase.containers.

For instance, to open or create a container in a database:

  // open or create a container with a specific indexing policy
  final indexingPolicy = IndexingPolicy(indexingMode: IndexingMode.consistent)
    ..excludedPaths.add(IndexPath('/*'))
    ..includedPaths.add(IndexPath('/"due-date"/?'))
    ..compositeIndexes.add([
      IndexPath('/label', order: IndexOrder.ascending),
      IndexPath('/"due-date"', order: IndexOrder.descending)
    ]);

  final todoCollection = await database.containers.openOrCreate(
    'todo_by_id',
    partitionKey: PartitionKeySpec.id,
    indexingPolicy: indexingPolicy,
  );

Data in containers is organized according to a partition key. azure_cosmosdb provides a built-in PartitionKeySpec.id (where the partition key is based on the id property), but custom partition keys may be specified when creating the container.

Data can also be indexed according to an indexing policy. By default, Cosmos DB automatically indexes all document properties.

Managing Documents in a Cosmos DB Container

azure_cosmosdb provides base classes BaseDocument and BaseDocumentWithEtag to model the data you need to store in Azure Cosmos DB.

These base classes impement the id property as well as an abstract toJson() method returning a Map<String, dynamic> JSON object. Derived classes must provide the implementation for toJson() and must also implement a static fromJson(Map json) method to rebuild instances from the Map JSON objects returned by Azure Cosmos DB. Implementations can be manual or generated, e.g. via package json_serializable.

For instance:

class ToDo extends BaseDocumentWithEtag {
  ToDo._(
    this.id,
    this.label,
    this.description,
    this.dueDate,
    this.completedDate,
  );

  ToDo(
    String label, {
    String? description,
    DateTime? dueDate,
    DateTime? completedDate,
  }) : this._(autoId(), label, description, dueDate, completedDate);

  @override
  final String id;

  String label;
  String? description;
  DateTime? dueDate;
  DateTime? completedDate;

  @override
  Map<String, dynamic> toJson() => {
        'id': id,
        'label': label,
        if (description != null) 'description': description,
        'due-date': dueDate?.toUtc().toIso8601String(),
        'completed': completedDate?.toUtc().toIso8601String(),
      };

  static ToDo fromJson(Map json) {
    final todo = ToDo._(
      json['id'],
      json['label'],
      json['description'],
      DateTime.tryParse(json['due-date'] ?? '')?.toLocal(),
      DateTime.tryParse(json['completed'] ?? '')?.toLocal(),
    );
    todo.setEtag(json);
    return todo;
  }
}

The CosmosDbContainer class provides methods for CRUD operations (create/upsert, replace/update/patch, find and delete). It also provides the CosmosDbContainer.query() method to search for documents in Cosmos DB using SQL-like queries.

For instance:

  // register the builder for ToDo items
  todoCollection.registerBuilder<ToDo>(ToDo.fromJson);

  // create a new item and save it to Cosmos DB
  var today = DateTime.now();
  today = DateTime(today.year, today.month, today.day);
  final task = await todoCollection.add(ToDo(
    'Me',
    'Improve tests',
    dueDate: today.add(Duration(days: 3)),
  ));

  // query the collection
  final otherTasks = await todoCollection.query<ToDo>(
    Query('SELECT * FROM c WHERE c.id != @id', params: {'@id': task.id}),
  );

Batch Operations

Batch operations on a container are supported via CosmosDbContainer.prepare*Batch(). These methods return a Batch object which will contain the individual operations to be executed. Operations are sent to Cosmos DB via Batch.execute().

CosmosDbContainer.prepareBatch() creates a TransactionalBatch instance where changes are applied and persisted one after the other. The behavior can be customised with the continueOnError flag. If an error occurs and continueOnError is false, processing is stopped and all subsequent operations will also fail with statusCode = HttpStatusCode.failedDependency. But if continueOnError is true, the subsequent operations will be executed anyway (and might eventually fail individually). Please note that operations in a TransactionalBatch must target documents in a single partition key. A PartitionKeyException will be thrown when calling TransactionalBatch.execute() if this is not the case.

Batch operations can be executed atomically where all operations succeed or all fail; to create an atomic batch, call CosmosDbContainer.prepareAtomicBatch(). Atomic batches have their continueOnError flag forced to false. If an error occurs, the unsuccessfull operation will report its own status code and all other operations will fail with statusCode = HttpStatusCode.failedDependency.

For instance:

  // open or create the container
  final todoCollection = await database.containers.openOrCreate(
    'todo_by_owner',
    partitionKey: PartitionKeySpec('/owner'), // partition key based on the "owner" field
  );

  // register the builder for ToDo items
  todoCollection.registerBuilder<ToDo>(ToDo.fromJson);
  print('Container ready.');

  final teams = ['DevTeam1', 'DevTeam2', 'DevTeam3'];
  final owner = teams[rnd.nextInt(teams.length)];

  // create new items and batch-save them to Cosmos DB
  var today = DateTime.now();
  today = DateTime(today.year, today.month, today.day);
  final batch = todoCollection.prepareAtomicBatch();
  final count = 1 + rnd.nextInt(4);
  for (var i = 0; i < count; i++) {
    batch.create(ToDo(
      owner,
      getLabel(),
      dueDate: today.add(Duration(days: rnd.nextInt(5))),
    ));
  }
  final newTasks = await batch.execute();

This package also implements cross-partition batches via CosmosDbContainer.prepareCrossPartitionBatch() which returns a CrossPartitionBatch instance. When CrossPartitionBatch.execute() is called, operations are grouped by partition keys and submitted to Cosmos DB separately. Cross-partition batches have their flages forced to isAtomic = false and continueOnError = true.

Please note that Cosmos DB has a limit of 100 operations per batch. TransactionalBatch instances enforce this limit when isAtomic = true. For non-atomic batches, more than 100 operations can be registered with the Batch instance and operations will be sent to Cosmos DB in chunks of 100 operations. If continueOnError = false and a chunk operation fails, subsequent chunks are not sent to CosmosDB and those operations will complete immediately with statusCode = HttpStatusCode.failedDependency.

Users and Permissions

Most APIs implemented in azure_cosmosdb support an optional CosmosDbPermission parameter when calling Azure Cosmos DB.

This makes it possible to open a connection to Azure Cosmos DB without providing the master key. The master key should be kept secret and should not be provided in Web apps or even mobile apps.

Azure Cosmos DB manages a list of users and permissions at the database level. If you need to implement direct access from a Web or mobile app to Azure Cosmos DB, you should create a user for your app and grant permissions as necessary.

To retrieve the permission in your app, you should implement a REST API, e.g. an Azure Function, that your app will call to get the required set of permissions. Only the REST API will need to know the master key to retrieve the permissions.

Disclaimer

Please note that this library is not supported nor endorsed by Microsoft.