From 2e9dcf49a8a5bd546d29d07201ff70637ced4ede Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Thu, 12 Feb 2026 11:19:42 -0800 Subject: [PATCH] feat(cosmos): add @bytelyst/cosmos package - Singleton CosmosClient with env var config (COSMOS_ENDPOINT, COSMOS_KEY, COSMOS_DATABASE) - Simple getContainer() for services - Container registry with registerContainers(), getRegisteredContainer(), initializeAllContainers() for dashboards - ContainerConfig type with partitionKeyPath and optional defaultTtl - _resetClient() and _resetRegistry() for test isolation - Peer dep: @azure/cosmos >=4.0.0 --- packages/cosmos/package.json | 21 +++++++++ packages/cosmos/src/client.ts | 54 +++++++++++++++++++++++ packages/cosmos/src/containers.ts | 71 +++++++++++++++++++++++++++++++ packages/cosmos/src/index.ts | 8 ++++ packages/cosmos/src/types.ts | 4 ++ packages/cosmos/tsconfig.json | 9 ++++ 6 files changed, 167 insertions(+) create mode 100644 packages/cosmos/package.json create mode 100644 packages/cosmos/src/client.ts create mode 100644 packages/cosmos/src/containers.ts create mode 100644 packages/cosmos/src/index.ts create mode 100644 packages/cosmos/src/types.ts create mode 100644 packages/cosmos/tsconfig.json diff --git a/packages/cosmos/package.json b/packages/cosmos/package.json new file mode 100644 index 00000000..19e24852 --- /dev/null +++ b/packages/cosmos/package.json @@ -0,0 +1,21 @@ +{ + "name": "@bytelyst/cosmos", + "version": "0.1.0", + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": ["dist"], + "scripts": { + "build": "tsc", + "test": "vitest run" + }, + "peerDependencies": { + "@azure/cosmos": ">=4.0.0" + } +} diff --git a/packages/cosmos/src/client.ts b/packages/cosmos/src/client.ts new file mode 100644 index 00000000..d7ca2d2b --- /dev/null +++ b/packages/cosmos/src/client.ts @@ -0,0 +1,54 @@ +/** + * Azure Cosmos DB client singleton. + * + * Reads COSMOS_ENDPOINT, COSMOS_KEY, and COSMOS_DATABASE from process.env. + * Provides getCosmosClient(), getDatabase(), and getContainer() for simple usage. + */ + +import { CosmosClient, Database, Container } from "@azure/cosmos"; + +let _client: CosmosClient | null = null; +let _database: Database | null = null; + +function getEnvOrThrow(name: string): string { + const value = process.env[name]; + if (!value) { + throw new Error(`Environment variable ${name} is required`); + } + return value; +} + +export function getCosmosClient(): CosmosClient { + if (!_client) { + _client = new CosmosClient({ + endpoint: getEnvOrThrow("COSMOS_ENDPOINT"), + key: getEnvOrThrow("COSMOS_KEY"), + }); + } + return _client; +} + +export function getDatabase(): Database { + if (!_database) { + const dbId = process.env.COSMOS_DATABASE || "lysnrai"; + _database = getCosmosClient().database(dbId); + } + return _database; +} + +/** + * Get a container by name. Uses the singleton database. + * For simple services that don't need a container registry. + */ +export function getContainer(name: string): Container { + return getDatabase().container(name); +} + +/** + * Reset the singleton (useful for testing). + * @internal + */ +export function _resetClient(): void { + _client = null; + _database = null; +} diff --git a/packages/cosmos/src/containers.ts b/packages/cosmos/src/containers.ts new file mode 100644 index 00000000..eea0446b --- /dev/null +++ b/packages/cosmos/src/containers.ts @@ -0,0 +1,71 @@ +/** + * Container registry for dashboards that need partition key validation + * and createIfNotExists support. + */ + +import { Container, PartitionKeyDefinition } from "@azure/cosmos"; +import { getDatabase, getCosmosClient } from "./client.js"; +import type { ContainerConfig } from "./types.js"; + +const _registry: Map = new Map(); +const _containerCache: Map = new Map(); + +/** + * Register containers with their partition key configuration. + * Call once at app startup before any getRegisteredContainer() calls. + */ +export function registerContainers( + definitions: Record, +): void { + for (const [name, config] of Object.entries(definitions)) { + _registry.set(name, config); + } +} + +/** + * Get a container that was previously registered. + * Throws if the container name is unknown. + */ +export function getRegisteredContainer(name: string): Container { + if (!_registry.has(name)) { + throw new Error( + `Unknown container '${name}'. Valid: ${[..._registry.keys()].join(", ")}`, + ); + } + + let container = _containerCache.get(name); + if (!container) { + container = getDatabase().container(name); + _containerCache.set(name, container); + } + return container; +} + +/** + * Create all registered containers if they don't exist. + * Call from a seed script or on first deploy. + */ +export async function initializeAllContainers(): Promise { + const client = getCosmosClient(); + const dbId = process.env.COSMOS_DATABASE || "lysnrai"; + const { database } = await client.databases.createIfNotExists({ id: dbId }); + + for (const [name, config] of _registry.entries()) { + await database.containers.createIfNotExists({ + id: name, + partitionKey: { + paths: [config.partitionKeyPath], + } as PartitionKeyDefinition, + ...(config.defaultTtl != null && { defaultTtl: config.defaultTtl }), + }); + } +} + +/** + * Reset the registry (useful for testing). + * @internal + */ +export function _resetRegistry(): void { + _registry.clear(); + _containerCache.clear(); +} diff --git a/packages/cosmos/src/index.ts b/packages/cosmos/src/index.ts new file mode 100644 index 00000000..8acda9e9 --- /dev/null +++ b/packages/cosmos/src/index.ts @@ -0,0 +1,8 @@ +export { getCosmosClient, getDatabase, getContainer, _resetClient } from "./client.js"; +export { + registerContainers, + getRegisteredContainer, + initializeAllContainers, + _resetRegistry, +} from "./containers.js"; +export type { ContainerConfig } from "./types.js"; diff --git a/packages/cosmos/src/types.ts b/packages/cosmos/src/types.ts new file mode 100644 index 00000000..59658f63 --- /dev/null +++ b/packages/cosmos/src/types.ts @@ -0,0 +1,4 @@ +export interface ContainerConfig { + partitionKeyPath: string; + defaultTtl?: number | null; +} diff --git a/packages/cosmos/tsconfig.json b/packages/cosmos/tsconfig.json new file mode 100644 index 00000000..5edad813 --- /dev/null +++ b/packages/cosmos/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["src/**/*.test.ts"] +}