azure-cosmos-ts
Azure Cosmos DB JavaScript/TypeScript SDK (@azure/cosmos) for data plane operations. Use for CRUD operations on documents, queries, bulk operations, and container management. Triggers: "Cosmos DB", "@azure/cosmos", "CosmosClient", "document CRUD", "NoSQL queries", "bulk operation
Install
npx skills add https://github.com/microsoft/skills/tree/main/.github/plugins/azure-sdk-typescript/skills/azure-cosmos-ts
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install microsoft-skills@llmmart
git clone https://github.com/microsoft/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole microsoft/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
@azure/cosmos (TypeScript/JavaScript)
Data plane SDK for Azure Cosmos DB NoSQL API operations — CRUD on documents, queries, bulk operations.
⚠️ Data vs Management Plane
- This SDK (@azure/cosmos): CRUD operations on documents, queries, stored procedures
- Management SDK (@azure/arm-cosmosdb): Create accounts, databases, containers via ARM
Installation
npm install @azure/cosmos @azure/identity
Current Version: 4.9.0
Node.js: >= 20.0.0
Environment Variables
COSMOS_ENDPOINT=https://<account>.documents.azure.com:443/
COSMOS_DATABASE=<database-name>
COSMOS_CONTAINER=<container-name>
# For key-based auth only (prefer AAD)
COSMOS_KEY=<account-key>
AZURE_TOKEN_CREDENTIALS=prod # Required only if DefaultAzureCredential is used in production
Authentication
Microsoft Entra Token Credential (Recommended)
import { CosmosClient } from "@azure/cosmos";
import { DefaultAzureCredential, ManagedIdentityCredential } from "@azure/identity";
// Local dev: DefaultAzureCredential. Production: set AZURE_TOKEN_CREDENTIALS=prod or AZURE_TOKEN_CREDENTIALS=<specific_credential>
const credential = new DefaultAzureCredential({requiredEnvVars: ["AZURE_TOKEN_CREDENTIALS"]});
// Or use a specific credential directly in production:
// See 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,
});
Key-Based Authentication
import { CosmosClient } from "@azure/cosmos";
// Option 1: Endpoint + Key
const client = new CosmosClient({
endpoint: process.env.COSMOS_ENDPOINT!,
key: process.env.COSMOS_KEY!,
});
// Option 2: Connection String
const client = new CosmosClient(process.env.COSMOS_CONNECTION_STRING!);
Resource Hierarchy
CosmosClient
└── Database
└── Container
├── Items (documents)
├── Scripts (stored procedures, triggers, UDFs)
└── Conflicts
Core Operations
Database & Container Setup
const { database } = await client.databases.createIfNotExists({
id: "my-database",
});
const { container } = await database.containers.createIfNotExists({
id: "my-container",
partitionKey: { paths: ["/partitionKey"] },
});
Create 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);
Read Document
const { resource } = await container
.item("product-1", "electronics") // id, partitionKey
.read<Product>();
if (resource) {
console.log(resource.name);
}
Update Document (Replace)
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 Document
const item: Product = {
id: "product-1",
partitionKey: "electronics",
name: "Laptop Pro",
price: 1299.99,
};
const { resource } = await container.items.upsert<Product>(item);
Delete Document
await container.item("product-1", "electronics").delete();
Patch Document (Partial Update)
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);
Queries
Simple Query
const { resources } = await container.items
.query<Product>("SELECT * FROM c WHERE c.price < 1000")
.fetchAll();
Parameterized Query (Recommended)
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();
Query with Pagination
const queryIterator = container.items.query<Product>(querySpec, {
maxItemCount: 10, // Items per page
});
while (queryIterator.hasMoreResults()) {
const { resources, continuationToken } = await queryIterator.fetchNext();
console.log(`Page with ${resources?.length} items`);
// Use continuationToken for next page if needed
}
Cross-Partition Query
const { resources } = await container.items
.query<Product>(
"SELECT * FROM c WHERE c.price > 500",
{ enableCrossPartitionQuery: true }
)
.fetchAll();
Bulk Operations
Execute Bulk Operations
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(`Operation ${index} succeeded`);
} else {
console.error(`Operation ${index} failed: ${result.statusCode}`);
}
});
Partition Keys
Simple Partition Key
const { container } = await database.containers.createIfNotExists({
id: "products",
partitionKey: { paths: ["/category"] },
});
Hierarchical Partition Key (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,
},
});
// Operations require array of partition key values
const { resource } = await container.items.create({
id: "order-1",
tenantId: "tenant-a",
userId: "user-123",
sessionId: "session-xyz",
total: 99.99,
});
// Read with hierarchical partition key
const { resource: order } = await container
.item("order-1", ["tenant-a", "user-123", "session-xyz"])
.read();
Error Handling
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 not found");
break;
case 409:
console.log("Conflict - document already exists");
break;
case 412:
console.log("Precondition failed (ETag mismatch)");
break;
case 429:
console.log("Rate limited - retry after:", error.retryAfterInMs);
break;
default:
console.error(`Cosmos error ${error.code}: ${error.message}`);
}
}
throw error;
}
Optimistic Concurrency (ETags)
// Read with ETag
const { resource, etag } = await container
.item("product-1", "electronics")
.read<Product>();
if (resource && etag) {
resource.price = 899.99;
try {
// Replace only if ETag matches
await container.item("product-1", "electronics").replace(resource, {
accessCondition: { type: "IfMatch", condition: etag },
});
} catch (error) {
if (error instanceof ErrorResponse && error.code === 412) {
console.log("Document was modified by another process");
}
}
}
TypeScript Types Reference
import {
// Client & Resources
CosmosClient,
Database,
Container,
Item,
Items,
// Operations
OperationInput,
BulkOperationType,
PatchOperation,
// Queries
SqlQuerySpec,
SqlParameter,
FeedOptions,
// Partition Keys
PartitionKeyDefinition,
PartitionKeyDefinitionVersion,
PartitionKeyKind,
// Responses
ItemResponse,
FeedResponse,
ResourceResponse,
// Errors
ErrorResponse,
} from "@azure/cosmos";
Best Practices
- Use Microsoft Entra Token Credential — Use
DefaultAzureCredentialfor local development; useManagedIdentityCredentialorWorkloadIdentityCredentialfor production - Always use parameterized queries — Prevents injection, improves plan caching
- Specify partition key — Avoid cross-partition queries when possible
- Use bulk operations — For multiple writes, use
executeBulkOperations - Handle 429 errors — Implement retry logic with exponential backoff
- Use ETags for concurrency — Prevent lost updates in concurrent scenarios
- Close client on shutdown — Call
client.dispose()in cleanup
Common Patterns
Service Layer Pattern
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;
}
}
Related SDKs
| SDK | Purpose | Install |
|---|---|---|
@azure/cosmos |
Data plane (this SDK) | npm install @azure/cosmos |
@azure/arm-cosmosdb |
Management plane (ARM) | npm install @azure/arm-cosmosdb |
@azure/identity |
Authentication | npm install @azure/identity |
Files (skills)
-
references
-
bulk-operations.md 11.2 KB
# Bulk Operations Reference High-throughput bulk operations for Azure Cosmos DB using the @azure/cosmos TypeScript SDK. ## Overview Bulk operations enable efficient batch processing of Create, Upsert, Read, Replace, Delete, and Patch operations with automatic batching and retry handling. ## BulkOperationType Enum ```typescript import { BulkOperationType } from "@azure/cosmos"; enum BulkOperationType { Create = "Create", Upsert = "Upsert", Read = "Read", Replace = "Replace", Delete = "Delete", Patch = "Patch" } ``` ## OperationInput Interface ```typescript import { OperationInput, PatchOperation } from "@azure/cosmos"; // Base operation types interface CreateOperationInput { operationType: BulkOperationType.Create; resourceBody: Record<string, unknown>; partitionKey?: PartitionKey; } interface UpsertOperationInput { operationType: BulkOperationType.Upsert; resourceBody: Record<string, unknown>; partitionKey?: PartitionKey; } interface ReadOperationInput { operationType: BulkOperationType.Read; id: string; partitionKey: PartitionKey; } interface ReplaceOperationInput { operationType: BulkOperationType.Replace; id: string; resourceBody: Record<string, unknown>; partitionKey?: PartitionKey; } interface DeleteOperationInput { operationType: BulkOperationType.Delete; id: string; partitionKey: PartitionKey; } interface PatchOperationInput { operationType: BulkOperationType.Patch; id: string; partitionKey: PartitionKey; resourceBody: { operations: PatchOperation[]; condition?: string; }; } type OperationInput = | CreateOperationInput | UpsertOperationInput | ReadOperationInput | ReplaceOperationInput | DeleteOperationInput | PatchOperationInput; ``` ## Basic Bulk Operations ```typescript import { CosmosClient, BulkOperationType, OperationInput, BulkOperationResponse } from "@azure/cosmos"; const client = new CosmosClient({ endpoint, key }); const container = client.database("mydb").container("mycontainer"); // Mixed bulk operations const operations: OperationInput[] = [ // Create { operationType: BulkOperationType.Create, resourceBody: { id: "item-1", partitionKey: "category-a", name: "Item 1", price: 10.99 } }, // Upsert { operationType: BulkOperationType.Upsert, resourceBody: { id: "item-2", partitionKey: "category-a", name: "Item 2", price: 20.99 } }, // Read { operationType: BulkOperationType.Read, id: "item-3", partitionKey: "category-b" }, // Replace { operationType: BulkOperationType.Replace, id: "item-4", partitionKey: "category-b", resourceBody: { id: "item-4", partitionKey: "category-b", name: "Updated Item 4", price: 15.99 } }, // Delete { operationType: BulkOperationType.Delete, id: "item-5", partitionKey: "category-c" } ]; const response: BulkOperationResponse = await container.items.bulk(operations); // Process results response.forEach((result, index) => { if (result.statusCode >= 200 && result.statusCode < 300) { console.log(`Operation ${index} succeeded: ${result.statusCode}`); if (result.resourceBody) { console.log(` Resource: ${JSON.stringify(result.resourceBody)}`); } } else { console.error(`Operation ${index} failed: ${result.statusCode}`); } }); ``` ## Bulk Create Pattern ```typescript interface Product { id: string; partitionKey: string; name: string; price: number; } async function bulkCreate( container: Container, items: Product[] ): Promise<{ succeeded: number; failed: number }> { const operations: OperationInput[] = items.map(item => ({ operationType: BulkOperationType.Create, resourceBody: item })); const response = await container.items.bulk(operations); let succeeded = 0; let failed = 0; response.forEach((result, index) => { if (result.statusCode === 201) { succeeded++; } else { failed++; console.error(`Failed to create item ${items[index].id}: ${result.statusCode}`); } }); return { succeeded, failed }; } // Usage const products: Product[] = [ { id: "p1", partitionKey: "electronics", name: "Laptop", price: 999 }, { id: "p2", partitionKey: "electronics", name: "Phone", price: 599 }, { id: "p3", partitionKey: "clothing", name: "Shirt", price: 29 }, ]; const result = await bulkCreate(container, products); console.log(`Created: ${result.succeeded}, Failed: ${result.failed}`); ``` ## Bulk Upsert Pattern ```typescript async function bulkUpsert<T extends { id: string }>( container: Container, items: T[] ): Promise<BulkOperationResponse> { const operations: OperationInput[] = items.map(item => ({ operationType: BulkOperationType.Upsert, resourceBody: item as Record<string, unknown> })); return container.items.bulk(operations); } // Usage - creates if not exists, updates if exists const updates = [ { id: "p1", partitionKey: "electronics", name: "Laptop Pro", price: 1299 }, { id: "p4", partitionKey: "electronics", name: "Tablet", price: 399 }, ]; await bulkUpsert(container, updates); ``` ## Bulk Delete Pattern ```typescript interface DeleteItem { id: string; partitionKey: string; } async function bulkDelete( container: Container, items: DeleteItem[] ): Promise<{ deleted: number; notFound: number; errors: number }> { const operations: OperationInput[] = items.map(item => ({ operationType: BulkOperationType.Delete, id: item.id, partitionKey: item.partitionKey })); const response = await container.items.bulk(operations); let deleted = 0; let notFound = 0; let errors = 0; response.forEach((result) => { if (result.statusCode === 204) { deleted++; } else if (result.statusCode === 404) { notFound++; } else { errors++; } }); return { deleted, notFound, errors }; } // Usage const toDelete: DeleteItem[] = [ { id: "p1", partitionKey: "electronics" }, { id: "p2", partitionKey: "electronics" }, ]; const deleteResult = await bulkDelete(container, toDelete); console.log(`Deleted: ${deleteResult.deleted}, Not found: ${deleteResult.notFound}`); ``` ## Bulk Patch Pattern ```typescript import { PatchOperation } from "@azure/cosmos"; interface PatchItem { id: string; partitionKey: string; operations: PatchOperation[]; } async function bulkPatch( container: Container, items: PatchItem[] ): Promise<BulkOperationResponse> { const operations: OperationInput[] = items.map(item => ({ operationType: BulkOperationType.Patch, id: item.id, partitionKey: item.partitionKey, resourceBody: { operations: item.operations } })); return container.items.bulk(operations); } // Usage - partial updates const patches: PatchItem[] = [ { id: "p1", partitionKey: "electronics", operations: [ { op: "replace", path: "/price", value: 899 }, { op: "add", path: "/onSale", value: true } ] }, { id: "p2", partitionKey: "electronics", operations: [ { op: "incr", path: "/viewCount", value: 1 } ] } ]; await bulkPatch(container, patches); ``` ## Chunked Bulk Operations For very large datasets, process in chunks to manage memory and handle rate limiting: ```typescript async function bulkOperationsChunked<T extends Record<string, unknown>>( container: Container, items: T[], operationType: BulkOperationType.Create | BulkOperationType.Upsert, chunkSize: number = 100 ): Promise<{ succeeded: number; failed: number }> { let succeeded = 0; let failed = 0; // Process in chunks for (let i = 0; i < items.length; i += chunkSize) { const chunk = items.slice(i, i + chunkSize); const operations: OperationInput[] = chunk.map(item => ({ operationType, resourceBody: item })); const response = await container.items.bulk(operations); response.forEach(result => { if (result.statusCode >= 200 && result.statusCode < 300) { succeeded++; } else { failed++; } }); console.log(`Processed ${Math.min(i + chunkSize, items.length)}/${items.length}`); } return { succeeded, failed }; } // Usage const largeDataset = generateItems(10000); const result = await bulkOperationsChunked(container, largeDataset, BulkOperationType.Create, 100); ``` ## Bulk Operation Response ```typescript interface BulkOperationResponse extends Array<OperationResponse> {} interface OperationResponse { /** HTTP status code */ statusCode: number; /** Request charge (RUs) for this operation */ requestCharge: number; /** ETag of the resource (for successful operations) */ eTag?: string; /** Resource body (for read/create/upsert/replace) */ resourceBody?: Record<string, unknown>; } // Common status codes // 200 - Read successful // 201 - Create successful // 204 - Delete successful // 400 - Bad request // 404 - Not found // 409 - Conflict (duplicate id on create) // 412 - Precondition failed // 429 - Rate limited ``` ## Error Handling ```typescript async function bulkWithRetry( container: Container, operations: OperationInput[], maxRetries: number = 3 ): Promise<BulkOperationResponse> { let response = await container.items.bulk(operations); let retryCount = 0; while (retryCount < maxRetries) { const failedOps: OperationInput[] = []; const failedIndices: number[] = []; response.forEach((result, index) => { if (result.statusCode === 429) { // Rate limited - retry these failedOps.push(operations[index]); failedIndices.push(index); } }); if (failedOps.length === 0) { break; // All succeeded } console.log(`Retrying ${failedOps.length} rate-limited operations...`); // Exponential backoff await new Promise(resolve => setTimeout(resolve, Math.pow(2, retryCount) * 1000) ); const retryResponse = await container.items.bulk(failedOps); // Update original response with retry results retryResponse.forEach((result, i) => { response[failedIndices[i]] = result; }); retryCount++; } return response; } ``` ## Best Practices 1. **Batch by partition key** — Operations in the same partition are more efficient 2. **Use appropriate chunk sizes** — 100-500 items per batch is typical 3. **Handle 429 errors** — Implement retry with exponential backoff 4. **Monitor RU consumption** — Check `requestCharge` in responses 5. **Use Upsert for idempotency** — Safer than Create for retry scenarios 6. **Validate before bulk** — Check data before submitting large batches 7. **Process results** — Always check individual operation status codes ## Performance Considerations | Factor | Recommendation | |--------|----------------| | Chunk size | 100-500 items per batch | | Partition distribution | Spread across partitions for parallelism | | Operation type | Upsert is safer than Create for retries | | Error handling | Implement retry logic for 429s | | Memory | Stream large datasets, don't load all into memory | ## See Also - [Query Patterns Reference](./query-patterns.md) - [Cosmos DB Bulk Execution](https://learn.microsoft.com/azure/cosmos-db/nosql/tutorial-dotnet-bulk-import) - [Request Units (RUs)](https://learn.microsoft.com/azure/cosmos-db/request-units) -
query-patterns.md 8.7 KB
# Query Patterns Reference Advanced query patterns for Azure Cosmos DB using the @azure/cosmos TypeScript SDK. ## Overview Cosmos DB supports SQL-like queries with support for JSON documents. This reference covers parameterized queries, pagination, cross-partition queries, and advanced query patterns. ## SqlQuerySpec Interface ```typescript import { SqlQuerySpec, SqlParameter } from "@azure/cosmos"; interface SqlQuerySpec { /** SQL query text */ query: string; /** Array of parameters */ parameters?: SqlParameter[]; } interface SqlParameter { /** Parameter name (including @) */ name: string; /** Parameter value */ value: unknown; } ``` ## Parameterized Queries (Recommended) Always use parameterized queries to prevent injection and improve plan caching. ```typescript import { SqlQuerySpec, Container } from "@azure/cosmos"; interface Product { id: string; category: string; name: string; price: number; inStock: boolean; } // Single parameter const querySpec: SqlQuerySpec = { query: "SELECT * FROM c WHERE c.category = @category", parameters: [ { name: "@category", value: "electronics" } ] }; const { resources } = await container.items .query<Product>(querySpec) .fetchAll(); // Multiple parameters const rangeQuery: SqlQuerySpec = { query: ` SELECT * FROM c WHERE c.category = @category AND c.price >= @minPrice AND c.price <= @maxPrice AND c.inStock = @inStock `, parameters: [ { name: "@category", value: "electronics" }, { name: "@minPrice", value: 100 }, { name: "@maxPrice", value: 1000 }, { name: "@inStock", value: true } ] }; const { resources: filtered } = await container.items .query<Product>(rangeQuery) .fetchAll(); ``` ## Pagination with Continuation Tokens ```typescript import { FeedOptions } from "@azure/cosmos"; interface PagedResult<T> { items: T[]; continuationToken?: string; hasMore: boolean; } async function queryWithPagination<T>( container: Container, querySpec: SqlQuerySpec, pageSize: number, continuationToken?: string ): Promise<PagedResult<T>> { const options: FeedOptions = { maxItemCount: pageSize, continuationToken }; const queryIterator = container.items.query<T>(querySpec, options); const { resources, continuationToken: nextToken } = await queryIterator.fetchNext(); return { items: resources || [], continuationToken: nextToken, hasMore: !!nextToken }; } // Usage let page = await queryWithPagination<Product>( container, { query: "SELECT * FROM c ORDER BY c.createdAt DESC" }, 10 ); console.log(`Page 1: ${page.items.length} items`); while (page.hasMore) { page = await queryWithPagination<Product>( container, { query: "SELECT * FROM c ORDER BY c.createdAt DESC" }, 10, page.continuationToken ); console.log(`Next page: ${page.items.length} items`); } ``` ## Async Iterator Pattern ```typescript async function* queryAll<T>( container: Container, querySpec: SqlQuerySpec ): AsyncGenerator<T> { const queryIterator = container.items.query<T>(querySpec); while (queryIterator.hasMoreResults()) { const { resources } = await queryIterator.fetchNext(); if (resources) { for (const item of resources) { yield item; } } } } // Usage for await (const product of queryAll<Product>(container, querySpec)) { console.log(product.name); } ``` ## Cross-Partition Queries ```typescript // Enable cross-partition query when partition key is not specified const crossPartitionQuery: SqlQuerySpec = { query: "SELECT * FROM c WHERE c.price > @minPrice", parameters: [{ name: "@minPrice", value: 500 }] }; const { resources } = await container.items .query<Product>(crossPartitionQuery, { enableCrossPartitionQuery: true }) .fetchAll(); // Aggregate across partitions const aggregateQuery: SqlQuerySpec = { query: "SELECT VALUE COUNT(1) FROM c WHERE c.category = @category", parameters: [{ name: "@category", value: "electronics" }] }; const { resources: countResult } = await container.items .query<number>(aggregateQuery, { enableCrossPartitionQuery: true }) .fetchAll(); console.log(`Total count: ${countResult[0]}`); ``` ## Projection Queries ```typescript // Select specific fields const projectionQuery: SqlQuerySpec = { query: ` SELECT c.id, c.name, c.price, c.category FROM c WHERE c.inStock = true ` }; interface ProductSummary { id: string; name: string; price: number; category: string; } const { resources } = await container.items .query<ProductSummary>(projectionQuery) .fetchAll(); // Computed properties const computedQuery: SqlQuerySpec = { query: ` SELECT c.id, c.name, c.price, c.price * 0.9 AS discountedPrice, CONCAT(c.category, "-", c.id) AS sku FROM c ` }; ``` ## Array Queries (JOIN) ```typescript interface Order { id: string; customerId: string; items: OrderItem[]; } interface OrderItem { productId: string; quantity: number; price: number; } // Query items within arrays const arrayQuery: SqlQuerySpec = { query: ` SELECT o.id AS orderId, o.customerId, i.productId, i.quantity, i.price FROM orders o JOIN i IN o.items WHERE i.quantity > @minQuantity `, parameters: [{ name: "@minQuantity", value: 5 }] }; // Check if array contains value const containsQuery: SqlQuerySpec = { query: ` SELECT * FROM c WHERE ARRAY_CONTAINS(c.tags, @tag) `, parameters: [{ name: "@tag", value: "featured" }] }; ``` ## Aggregate Functions ```typescript // COUNT const countQuery = { query: "SELECT VALUE COUNT(1) FROM c" }; // SUM, AVG, MIN, MAX const statsQuery: SqlQuerySpec = { query: ` SELECT COUNT(1) AS totalProducts, SUM(c.price) AS totalValue, AVG(c.price) AS averagePrice, MIN(c.price) AS minPrice, MAX(c.price) AS maxPrice FROM c WHERE c.category = @category `, parameters: [{ name: "@category", value: "electronics" }] }; interface ProductStats { totalProducts: number; totalValue: number; averagePrice: number; minPrice: number; maxPrice: number; } const { resources } = await container.items .query<ProductStats>(statsQuery, { enableCrossPartitionQuery: true }) .fetchAll(); ``` ## ORDER BY and TOP ```typescript // Order by with TOP const topQuery: SqlQuerySpec = { query: ` SELECT TOP 10 * FROM c WHERE c.category = @category ORDER BY c.price DESC `, parameters: [{ name: "@category", value: "electronics" }] }; // Multiple ORDER BY const multiOrderQuery: SqlQuerySpec = { query: ` SELECT * FROM c ORDER BY c.category ASC, c.price DESC ` }; // OFFSET and LIMIT (alternative to continuation tokens) const offsetQuery: SqlQuerySpec = { query: ` SELECT * FROM c ORDER BY c.createdAt DESC OFFSET @offset LIMIT @limit `, parameters: [ { name: "@offset", value: 20 }, { name: "@limit", value: 10 } ] }; ``` ## String Functions ```typescript const stringQuery: SqlQuerySpec = { query: ` SELECT * FROM c WHERE CONTAINS(c.name, @searchTerm, true) OR STARTSWITH(c.name, @prefix) `, parameters: [ { name: "@searchTerm", value: "phone" }, { name: "@prefix", value: "Smart" } ] }; // Case-insensitive search with LOWER const caseInsensitiveQuery: SqlQuerySpec = { query: ` SELECT * FROM c WHERE LOWER(c.name) LIKE @pattern `, parameters: [{ name: "@pattern", value: "%laptop%" }] }; ``` ## FeedOptions Reference ```typescript interface FeedOptions { /** Max items per page */ maxItemCount?: number; /** Continuation token from previous page */ continuationToken?: string; /** Enable cross-partition queries */ enableCrossPartitionQuery?: boolean; /** Max parallelism for cross-partition queries */ maxDegreeOfParallelism?: number; /** Partition key for scoped queries */ partitionKey?: PartitionKey; /** Enable scan in queries (avoid if possible) */ enableScanInQuery?: boolean; /** Populate index metrics in response */ populateIndexMetrics?: boolean; } ``` ## Query Performance Tips 1. **Always specify partition key** — Avoids expensive cross-partition queries 2. **Use parameterized queries** — Enables query plan caching 3. **Project only needed fields** — Reduces response size and RU consumption 4. **Avoid cross-partition aggregates** — Very expensive; consider materialized views 5. **Use continuation tokens** — More efficient than OFFSET/LIMIT 6. **Check index metrics** — Use `populateIndexMetrics: true` to diagnose slow queries ## See Also - [Bulk Operations Reference](./bulk-operations.md) - [Azure Cosmos DB SQL Reference](https://learn.microsoft.com/azure/cosmos-db/nosql/query/) - [Query Performance Tips](https://learn.microsoft.com/azure/cosmos-db/nosql/query-metrics)
-
-
SKILL.md 11.6 KB
--- name: azure-cosmos-ts description: | Azure Cosmos DB JavaScript/TypeScript SDK (@azure/cosmos) for data plane operations. Use for CRUD operations on documents, queries, bulk operations, and container management. Triggers: "Cosmos DB", "@azure/cosmos", "CosmosClient", "document CRUD", "NoSQL queries", "bulk operations", "partition key", "container.items". license: MIT metadata: author: Microsoft version: "1.0.0" package: '@azure/cosmos' --- # @azure/cosmos (TypeScript/JavaScript) Data plane SDK for Azure Cosmos DB NoSQL API operations — CRUD on documents, queries, bulk operations. > **⚠️ Data vs Management Plane** > - **This SDK (@azure/cosmos)**: CRUD operations on documents, queries, stored procedures > - **Management SDK (@azure/arm-cosmosdb)**: Create accounts, databases, containers via ARM ## Installation ```bash npm install @azure/cosmos @azure/identity ``` **Current Version**: 4.9.0 **Node.js**: >= 20.0.0 ## Environment Variables ```bash COSMOS_ENDPOINT=https://<account>.documents.azure.com:443/ COSMOS_DATABASE=<database-name> COSMOS_CONTAINER=<container-name> # For key-based auth only (prefer AAD) COSMOS_KEY=<account-key> AZURE_TOKEN_CREDENTIALS=prod # Required only if DefaultAzureCredential is used in production ``` ## Authentication ### Microsoft Entra Token Credential (Recommended) ```typescript import { CosmosClient } from "@azure/cosmos"; import { DefaultAzureCredential, ManagedIdentityCredential } from "@azure/identity"; // Local dev: DefaultAzureCredential. Production: set AZURE_TOKEN_CREDENTIALS=prod or AZURE_TOKEN_CREDENTIALS=<specific_credential> const credential = new DefaultAzureCredential({requiredEnvVars: ["AZURE_TOKEN_CREDENTIALS"]}); // Or use a specific credential directly in production: // See 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, }); ``` ### Key-Based Authentication ```typescript import { CosmosClient } from "@azure/cosmos"; // Option 1: Endpoint + Key const client = new CosmosClient({ endpoint: process.env.COSMOS_ENDPOINT!, key: process.env.COSMOS_KEY!, }); // Option 2: Connection String const client = new CosmosClient(process.env.COSMOS_CONNECTION_STRING!); ``` ## Resource Hierarchy ``` CosmosClient └── Database └── Container ├── Items (documents) ├── Scripts (stored procedures, triggers, UDFs) └── Conflicts ``` ## Core Operations ### Database & Container Setup ```typescript const { database } = await client.databases.createIfNotExists({ id: "my-database", }); const { container } = await database.containers.createIfNotExists({ id: "my-container", partitionKey: { paths: ["/partitionKey"] }, }); ``` ### Create Document ```typescript 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); ``` ### Read Document ```typescript const { resource } = await container .item("product-1", "electronics") // id, partitionKey .read<Product>(); if (resource) { console.log(resource.name); } ``` ### Update Document (Replace) ```typescript 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 Document ```typescript const item: Product = { id: "product-1", partitionKey: "electronics", name: "Laptop Pro", price: 1299.99, }; const { resource } = await container.items.upsert<Product>(item); ``` ### Delete Document ```typescript await container.item("product-1", "electronics").delete(); ``` ### Patch Document (Partial Update) ```typescript 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); ``` ## Queries ### Simple Query ```typescript const { resources } = await container.items .query<Product>("SELECT * FROM c WHERE c.price < 1000") .fetchAll(); ``` ### Parameterized Query (Recommended) ```typescript 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(); ``` ### Query with Pagination ```typescript const queryIterator = container.items.query<Product>(querySpec, { maxItemCount: 10, // Items per page }); while (queryIterator.hasMoreResults()) { const { resources, continuationToken } = await queryIterator.fetchNext(); console.log(`Page with ${resources?.length} items`); // Use continuationToken for next page if needed } ``` ### Cross-Partition Query ```typescript const { resources } = await container.items .query<Product>( "SELECT * FROM c WHERE c.price > 500", { enableCrossPartitionQuery: true } ) .fetchAll(); ``` ## Bulk Operations ### Execute Bulk Operations ```typescript 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(`Operation ${index} succeeded`); } else { console.error(`Operation ${index} failed: ${result.statusCode}`); } }); ``` ## Partition Keys ### Simple Partition Key ```typescript const { container } = await database.containers.createIfNotExists({ id: "products", partitionKey: { paths: ["/category"] }, }); ``` ### Hierarchical Partition Key (MultiHash) ```typescript 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, }, }); // Operations require array of partition key values const { resource } = await container.items.create({ id: "order-1", tenantId: "tenant-a", userId: "user-123", sessionId: "session-xyz", total: 99.99, }); // Read with hierarchical partition key const { resource: order } = await container .item("order-1", ["tenant-a", "user-123", "session-xyz"]) .read(); ``` ## Error Handling ```typescript 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 not found"); break; case 409: console.log("Conflict - document already exists"); break; case 412: console.log("Precondition failed (ETag mismatch)"); break; case 429: console.log("Rate limited - retry after:", error.retryAfterInMs); break; default: console.error(`Cosmos error ${error.code}: ${error.message}`); } } throw error; } ``` ## Optimistic Concurrency (ETags) ```typescript // Read with ETag const { resource, etag } = await container .item("product-1", "electronics") .read<Product>(); if (resource && etag) { resource.price = 899.99; try { // Replace only if ETag matches await container.item("product-1", "electronics").replace(resource, { accessCondition: { type: "IfMatch", condition: etag }, }); } catch (error) { if (error instanceof ErrorResponse && error.code === 412) { console.log("Document was modified by another process"); } } } ``` ## TypeScript Types Reference ```typescript import { // Client & Resources CosmosClient, Database, Container, Item, Items, // Operations OperationInput, BulkOperationType, PatchOperation, // Queries SqlQuerySpec, SqlParameter, FeedOptions, // Partition Keys PartitionKeyDefinition, PartitionKeyDefinitionVersion, PartitionKeyKind, // Responses ItemResponse, FeedResponse, ResourceResponse, // Errors ErrorResponse, } from "@azure/cosmos"; ``` ## Best Practices 1. **Use Microsoft Entra Token Credential** — Use `DefaultAzureCredential` for local development; use `ManagedIdentityCredential` or `WorkloadIdentityCredential` for production 2. **Always use parameterized queries** — Prevents injection, improves plan caching 3. **Specify partition key** — Avoid cross-partition queries when possible 4. **Use bulk operations** — For multiple writes, use `executeBulkOperations` 5. **Handle 429 errors** — Implement retry logic with exponential backoff 6. **Use ETags for concurrency** — Prevent lost updates in concurrent scenarios 7. **Close client on shutdown** — Call `client.dispose()` in cleanup ## Common Patterns ### Service Layer Pattern ```typescript 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; } } ``` ## Related SDKs | SDK | Purpose | Install | |-----|---------|---------| | `@azure/cosmos` | Data plane (this SDK) | `npm install @azure/cosmos` | | `@azure/arm-cosmosdb` | Management plane (ARM) | `npm install @azure/arm-cosmosdb` | | `@azure/identity` | Authentication | `npm install @azure/identity` |
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.