azure-cosmos-ts

npx skills add https://github.com/microsoft/skills --skill azure-cosmos-ts

@azure/cosmos (TypeScript/JavaScript)

SDK du plan de données pour les opérations Azure Cosmos DB NoSQL API — CRUD sur documents, requêtes, opérations en masse.

⚠️ Plan de données vs Plan de gestion

  • Ce SDK (@azure/cosmos) : opérations CRUD sur documents, requêtes, procédures stockées
  • SDK de gestion (@azure/arm-cosmosdb) : créer des comptes, bases de données, conteneurs via ARM

Installation

npm install @azure/cosmos @azure/identity

Version actuelle : 4.9.0
Node.js : >= 20.0.0

Variables d'environnement

COSMOS_ENDPOINT=https://<account>.documents.azure.com:443/
COSMOS_DATABASE=<database-name>
COSMOS_CONTAINER=<container-name>
# Pour l'authentification par clé uniquement (préférer AAD)
COSMOS_KEY=<account-key>
AZURE_TOKEN_CREDENTIALS=prod # Obligatoire uniquement si DefaultAzureCredential est utilisé en production

Authentification

Credential de jeton Microsoft Entra (Recommandé)

import { CosmosClient } from "@azure/cosmos";
import { DefaultAzureCredential, ManagedIdentityCredential } from "@azure/identity";

// Dev local : DefaultAzureCredential. Production : définir AZURE_TOKEN_CREDENTIALS=prod ou AZURE_TOKEN_CREDENTIALS=<specific_credential>
const credential = new DefaultAzureCredential({requiredEnvVars: ["AZURE_TOKEN_CREDENTIALS"]});
// Ou utiliser un credential spécifique directement en production :
// Voir https://learn.microsoft.com/javascript/api/overview/azure/identity-readme?view=azure-node-latest#credential-classes
// const credential = new ManagedIdentityCredential();

const client = new CosmosClient({
  endpoint: process.env.COSMOS_ENDPOINT!,
  aadCredentials: credential,
});

Authentification par clé

import { CosmosClient } from "@azure/cosmos";

// Option 1 : Endpoint + Clé
const client = new CosmosClient({
  endpoint: process.env.COSMOS_ENDPOINT!,
  key: process.env.COSMOS_KEY!,
});

// Option 2 : Chaîne de connexion
const client = new CosmosClient(process.env.COSMOS_CONNECTION_STRING!);

Hiérarchie des ressources

CosmosClient
└── Database
    └── Container
        ├── Items (documents)
        ├── Scripts (procédures stockées, triggers, UDFs)
        └── Conflicts

Opérations essentielles

Configuration de base de données et conteneur

const { database } = await client.databases.createIfNotExists({
  id: "my-database",
});

const { container } = await database.containers.createIfNotExists({
  id: "my-container",
  partitionKey: { paths: ["/partitionKey"] },
});

Créer un document

interface Product {
  id: string;
  partitionKey: string;
  name: string;
  price: number;
}

const item: Product = {
  id: "product-1",
  partitionKey: "electronics",
  name: "Laptop",
  price: 999.99,
};

const { resource } = await container.items.create<Product>(item);

Lire un document

const { resource } = await container
  .item("product-1", "electronics") // id, partitionKey
  .read<Product>();

if (resource) {
  console.log(resource.name);
}

Mettre à jour un document (Remplacer)

const { resource: existing } = await container
  .item("product-1", "electronics")
  .read<Product>();

if (existing) {
  existing.price = 899.99;
  const { resource: updated } = await container
    .item("product-1", "electronics")
    .replace<Product>(existing);
}

Upsert de document

const item: Product = {
  id: "product-1",
  partitionKey: "electronics",
  name: "Laptop Pro",
  price: 1299.99,
};

const { resource } = await container.items.upsert<Product>(item);

Supprimer un document

await container.item("product-1", "electronics").delete();

Patch de document (Mise à jour partielle)

import { PatchOperation } from "@azure/cosmos";

const operations: PatchOperation[] = [
  { op: "replace", path: "/price", value: 799.99 },
  { op: "add", path: "/discount", value: true },
  { op: "remove", path: "/oldField" },
];

const { resource } = await container
  .item("product-1", "electronics")
  .patch<Product>(operations);

Requêtes

Requête simple

const { resources } = await container.items
  .query<Product>("SELECT * FROM c WHERE c.price < 1000")
  .fetchAll();

Requête paramétrée (Recommandé)

import { SqlQuerySpec } from "@azure/cosmos";

const querySpec: SqlQuerySpec = {
  query: "SELECT * FROM c WHERE c.partitionKey = @category AND c.price < @maxPrice",
  parameters: [
    { name: "@category", value: "electronics" },
    { name: "@maxPrice", value: 1000 },
  ],
};

const { resources } = await container.items
  .query<Product>(querySpec)
  .fetchAll();

Requête avec pagination

const queryIterator = container.items.query<Product>(querySpec, {
  maxItemCount: 10, // Éléments par page
});

while (queryIterator.hasMoreResults()) {
  const { resources, continuationToken } = await queryIterator.fetchNext();
  console.log(`Page avec ${resources?.length} éléments`);
  // Utiliser continuationToken pour la page suivante si nécessaire
}

Requête multi-partition

const { resources } = await container.items
  .query<Product>(
    "SELECT * FROM c WHERE c.price > 500",
    { enableCrossPartitionQuery: true }
  )
  .fetchAll();

Opérations en masse

Exécuter des opérations en masse

import { BulkOperationType, OperationInput } from "@azure/cosmos";

const operations: OperationInput[] = [
  {
    operationType: BulkOperationType.Create,
    resourceBody: { id: "1", partitionKey: "cat-a", name: "Item 1" },
  },
  {
    operationType: BulkOperationType.Upsert,
    resourceBody: { id: "2", partitionKey: "cat-a", name: "Item 2" },
  },
  {
    operationType: BulkOperationType.Read,
    id: "3",
    partitionKey: "cat-b",
  },
  {
    operationType: BulkOperationType.Replace,
    id: "4",
    partitionKey: "cat-b",
    resourceBody: { id: "4", partitionKey: "cat-b", name: "Updated" },
  },
  {
    operationType: BulkOperationType.Delete,
    id: "5",
    partitionKey: "cat-c",
  },
  {
    operationType: BulkOperationType.Patch,
    id: "6",
    partitionKey: "cat-c",
    resourceBody: {
      operations: [{ op: "replace", path: "/name", value: "Patched" }],
    },
  },
];

const response = await container.items.executeBulkOperations(operations);

response.forEach((result, index) => {
  if (result.statusCode >= 200 && result.statusCode < 300) {
    console.log(`Opération ${index} réussie`);
  } else {
    console.error(`Opération ${index} échouée : ${result.statusCode}`);
  }
});

Clés de partition

Clé de partition simple

const { container } = await database.containers.createIfNotExists({
  id: "products",
  partitionKey: { paths: ["/category"] },
});

Clé de partition hiérarchique (MultiHash)

import { PartitionKeyDefinitionVersion, PartitionKeyKind } from "@azure/cosmos";

const { container } = await database.containers.createIfNotExists({
  id: "orders",
  partitionKey: {
    paths: ["/tenantId", "/userId", "/sessionId"],
    version: PartitionKeyDefinitionVersion.V2,
    kind: PartitionKeyKind.MultiHash,
  },
});

// Les opérations requièrent un tableau de valeurs de clé de partition
const { resource } = await container.items.create({
  id: "order-1",
  tenantId: "tenant-a",
  userId: "user-123",
  sessionId: "session-xyz",
  total: 99.99,
});

// Lecture avec clé de partition hiérarchique
const { resource: order } = await container
  .item("order-1", ["tenant-a", "user-123", "session-xyz"])
  .read();

Gestion des erreurs

import { ErrorResponse } from "@azure/cosmos";

try {
  const { resource } = await container.item("missing", "pk").read();
} catch (error) {
  if (error instanceof ErrorResponse) {
    switch (error.code) {
      case 404:
        console.log("Document introuvable");
        break;
      case 409:
        console.log("Conflit - document déjà existant");
        break;
      case 412:
        console.log("Précondition échouée (ETag incompatible)");
        break;
      case 429:
        console.log("Limitation de débit - réessayer après :", error.retryAfterInMs);
        break;
      default:
        console.error(`Erreur Cosmos ${error.code} : ${error.message}`);
    }
  }
  throw error;
}

Concurrence optimiste (ETags)

// Lire avec ETag
const { resource, etag } = await container
  .item("product-1", "electronics")
  .read<Product>();

if (resource && etag) {
  resource.price = 899.99;

  try {
    // Remplacer uniquement si l'ETag correspond
    await container.item("product-1", "electronics").replace(resource, {
      accessCondition: { type: "IfMatch", condition: etag },
    });
  } catch (error) {
    if (error instanceof ErrorResponse && error.code === 412) {
      console.log("Le document a été modifié par un autre processus");
    }
  }
}

Référence des types TypeScript

import {
  // Client & Ressources
  CosmosClient,
  Database,
  Container,
  Item,
  Items,

  // Opérations
  OperationInput,
  BulkOperationType,
  PatchOperation,

  // Requêtes
  SqlQuerySpec,
  SqlParameter,
  FeedOptions,

  // Clés de partition
  PartitionKeyDefinition,
  PartitionKeyDefinitionVersion,
  PartitionKeyKind,

  // Réponses
  ItemResponse,
  FeedResponse,
  ResourceResponse,

  // Erreurs
  ErrorResponse,
} from "@azure/cosmos";

Bonnes pratiques

  1. Utiliser Microsoft Entra Token Credential — Utiliser DefaultAzureCredential pour le développement local ; utiliser ManagedIdentityCredential ou WorkloadIdentityCredential en production
  2. Toujours utiliser des requêtes paramétrées — Prévient l'injection, améliore la mise en cache du plan
  3. Spécifier la clé de partition — Éviter les requêtes multi-partition si possible
  4. Utiliser les opérations en masse — Pour plusieurs écritures, utiliser executeBulkOperations
  5. Gérer les erreurs 429 — Implémenter une logique de relance avec backoff exponentiel
  6. Utiliser les ETags pour la concurrence — Prévenir les pertes de mise à jour dans les scénarios concurrents
  7. Fermer le client à l'arrêt — Appeler client.dispose() lors du nettoyage

Modèles courants

Modèle de couche service

export class ProductService {
  private container: Container;

  constructor(client: CosmosClient) {
    this.container = client
      .database(process.env.COSMOS_DATABASE!)
      .container(process.env.COSMOS_CONTAINER!);
  }

  async getById(id: string, category: string): Promise<Product | null> {
    try {
      const { resource } = await this.container
        .item(id, category)
        .read<Product>();
      return resource ?? null;
    } catch (error) {
      if (error instanceof ErrorResponse && error.code === 404) {
        return null;
      }
      throw error;
    }
  }

  async create(product: Omit<Product, "id">): Promise<Product> {
    const item = { ...product, id: crypto.randomUUID() };
    const { resource } = await this.container.items.create<Product>(item);
    return resource!;
  }

  async findByCategory(category: string): Promise<Product[]> {
    const querySpec: SqlQuerySpec = {
      query: "SELECT * FROM c WHERE c.partitionKey = @category",
      parameters: [{ name: "@category", value: category }],
    };
    const { resources } = await this.container.items
      .query<Product>(querySpec)
      .fetchAll();
    return resources;
  }
}

SDK connexes

SDK Objectif Installation
@azure/cosmos Plan de données (ce SDK) npm install @azure/cosmos
@azure/arm-cosmosdb Plan de gestion (ARM) npm install @azure/arm-cosmosdb
@azure/identity Authentification npm install @azure/identity

Skills similaires