- Wrapped unquoted @scope/pkg values in double quotes across 19 SKILL.md files. - Added 'package' to ALLOWED_FIELDS in JS validator. - Added YAML validity regression test to test suite. - Updated package-lock.json. Fixes #79 Closes #80
472 lines
11 KiB
Markdown
472 lines
11 KiB
Markdown
---
|
|
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".
|
|
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>
|
|
```
|
|
|
|
## Authentication
|
|
|
|
### AAD with DefaultAzureCredential (Recommended)
|
|
|
|
```typescript
|
|
import { CosmosClient } from "@azure/cosmos";
|
|
import { DefaultAzureCredential } from "@azure/identity";
|
|
|
|
const client = new CosmosClient({
|
|
endpoint: process.env.COSMOS_ENDPOINT!,
|
|
aadCredentials: new DefaultAzureCredential(),
|
|
});
|
|
```
|
|
|
|
### 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 AAD authentication** — Prefer `DefaultAzureCredential` over keys
|
|
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` |
|