UNPKG

@cfworker/cosmos

Version:

Azure Cosmos DB client for Cloudflare Workers and service workers

283 lines (282 loc) 12.7 kB
import { FeedResponse, ItemResponse } from './response.js'; import { defaultRetryPolicy } from './retry.js'; import { DefaultSessionContainer } from './session.js'; import { getSigner } from './signer.js'; import { assertArg, escapeNonASCII, parseConnectionString, uri } from './util.js'; export class CosmosClient { endpoint; signer; retryPolicy; consistencyLevel; dbId; collId; enableCrossPartitionQueries; systemFetch; sessions; requestCharges = 0; retries = { count: 0, delayMs: 0 }; constructor(config) { let { endpoint, masterKey } = 'connectionString' in config ? parseConnectionString(config.connectionString) : config; if (endpoint.endsWith('/')) { endpoint = endpoint.slice(0, -1); } this.endpoint = endpoint; this.signer = getSigner(masterKey); this.retryPolicy = config.retryPolicy ?? defaultRetryPolicy; this.consistencyLevel = config.consistencyLevel ?? 'Session'; this.dbId = config.dbId; this.collId = config.collId; this.enableCrossPartitionQueries = config.enableCrossPartitionQueries ?? true; this.sessions = config.sessions ?? new DefaultSessionContainer(); this.systemFetch = config.fetch ?? fetch.bind(globalThis); } async getDatabases(args = {}) { const url = this.endpoint + `/dbs`; const request = new Request(url, { method: 'GET' }); this.setHeaders(request.headers, args); const response = await this.fetchWithRetry(request); const next = this.getNext(response, args, this.getDatabases); return new FeedResponse(response, next, 'Databases'); } async getDatabase(args = {}) { const { dbId = this.dbId, ...headers } = args; assertArg('dbId', dbId); const url = this.endpoint + uri `/dbs/${dbId}`; const request = new Request(url, { method: 'GET' }); this.setHeaders(request.headers, headers); const response = await this.fetchWithRetry(request); return new ItemResponse(response); } async getCollections(args = {}) { const { dbId = this.dbId, ...headers } = args; assertArg('dbId', dbId); const url = this.endpoint + uri `/dbs/${dbId}/colls`; const request = new Request(url, { method: 'GET' }); this.setHeaders(request.headers, headers); const response = await this.fetchWithRetry(request); const next = this.getNext(response, args, this.getCollections); return new FeedResponse(response, next, 'DocumentCollections'); } async getCollection(args = {}) { const { dbId = this.dbId, collId = this.collId, ...headers } = args; assertArg('dbId', dbId); assertArg('collId', collId); const url = this.endpoint + uri `/dbs/${dbId}/colls/${collId}`; const request = new Request(url, { method: 'GET' }); this.setHeaders(request.headers, headers); const response = await this.fetchWithRetry(request); return new ItemResponse(response); } async createCollection(args) { const { dbId = this.dbId, collId = this.collId, indexingPolicy, partitionKey, ...headers } = args; assertArg('dbId', dbId); assertArg('collId', collId); const url = this.endpoint + uri `/dbs/${dbId}/colls`; const body = JSON.stringify({ id: collId, indexingPolicy, partitionKey }); const request = new Request(url, { method: 'POST', body }); this.setHeaders(request.headers, headers); request.headers.set('content-type', 'application/json'); const response = await this.fetchWithRetry(request); return new ItemResponse(response); } async replaceCollection(args) { const { dbId = this.dbId, collId = this.collId, indexingPolicy, partitionKey, ...headers } = args; assertArg('dbId', dbId); assertArg('collId', collId); const url = this.endpoint + uri `/dbs/${dbId}/colls/${collId}`; const body = JSON.stringify({ id: collId, indexingPolicy, partitionKey }); const request = new Request(url, { method: 'PUT', body }); this.setHeaders(request.headers, headers); request.headers.set('content-type', 'application/json'); const response = await this.fetchWithRetry(request); return new ItemResponse(response); } async deleteCollection(args = {}) { const { dbId = this.dbId, collId = this.collId, ...headers } = args; assertArg('dbId', dbId); assertArg('collId', collId); const url = this.endpoint + uri `/dbs/${dbId}/colls/${collId}`; const request = new Request(url, { method: 'DELETE' }); this.setHeaders(request.headers, headers); const response = await this.fetchWithRetry(request); return response; } async getDocuments(args) { const { dbId = this.dbId, collId = this.collId, ...headers } = args; assertArg('dbId', dbId); assertArg('collId', collId); const url = this.endpoint + uri `/dbs/${dbId}/colls/${collId}/docs`; const request = new Request(url, { method: 'GET' }); this.setHeaders(request.headers, headers); const response = await this.fetchWithRetry(request); const next = this.getNext(response, args, this.getDocuments); return new FeedResponse(response, next, 'Documents'); } async getDocument(args) { const { dbId = this.dbId, collId = this.collId, docId, ...headers } = args; assertArg('dbId', dbId); assertArg('collId', collId); const url = this.endpoint + uri `/dbs/${dbId}/colls/${collId}/docs/${docId}`; const request = new Request(url, { method: 'GET' }); this.setHeaders(request.headers, headers); const response = await this.fetchWithRetry(request); return new ItemResponse(response); } async createDocument(args) { const { dbId = this.dbId, collId = this.collId, document, ...headers } = args; assertArg('dbId', dbId); assertArg('collId', collId); const url = this.endpoint + uri `/dbs/${dbId}/colls/${collId}/docs`; const body = toBodyInit(document); const request = new Request(url, { method: 'POST', body }); this.setHeaders(request.headers, headers); request.headers.set('content-type', 'application/json'); const response = await this.fetchWithRetry(request); return new ItemResponse(response); } async replaceDocument(args) { const { dbId = this.dbId, collId = this.collId, docId, document, ...headers } = args; assertArg('dbId', dbId); assertArg('collId', collId); const url = this.endpoint + uri `/dbs/${dbId}/colls/${collId}/docs/${docId}`; const body = toBodyInit(document); const request = new Request(url, { method: 'PUT', body }); this.setHeaders(request.headers, headers); request.headers.set('content-type', 'application/json'); const response = await this.fetchWithRetry(request); return new ItemResponse(response); } async deleteDocument(args) { const { dbId = this.dbId, collId = this.collId, docId, ...headers } = args; assertArg('dbId', dbId); assertArg('collId', collId); const url = this.endpoint + uri `/dbs/${dbId}/colls/${collId}/docs/${docId}`; const request = new Request(url, { method: 'DELETE' }); this.setHeaders(request.headers, headers); const response = await this.fetchWithRetry(request); return response; } async queryDocuments(args) { const { dbId = this.dbId, collId = this.collId, query, parameters = [], ...headerArgs } = args; assertArg('dbId', dbId); assertArg('collId', collId); const headers = Object.assign({ enableCrossPartition: this.enableCrossPartitionQueries, enableScan: false, maxItems: -1, populateMetrics: false, isQuery: true }, headerArgs); const url = this.endpoint + uri `/dbs/${dbId}/colls/${collId}/docs`; const body = JSON.stringify({ query, parameters }); const request = new Request(url, { method: 'POST', body }); this.setHeaders(request.headers, headers); request.headers.set('content-type', 'application/query+json'); const response = await this.fetchWithRetry(request); const next = this.getNext(response, args, this.queryDocuments); return new FeedResponse(response, next, 'Documents'); } getNext(response, args, fn) { return () => { const continuation = response.headers.get('x-ms-continuation'); if (!continuation) { throw new Error(`Response is not continuable.`); } return fn.call(this, Object.assign({}, args, { continuation })); }; } async fetchWithRetry(request, context) { const retryRequest = request.clone(); const response = await this.fetch(request); context ??= { attempts: 0, cumulativeWaitMs: 0, request, response }; context.attempts++; context.request = retryRequest; context.response = response; const { retry, delayMs } = await this.retryPolicy.shouldRetry(context); if (!retry) { retryRequest.body?.cancel(); return response; } if (delayMs !== 0) { await new Promise(resolve => setTimeout(resolve, delayMs)); context.cumulativeWaitMs += delayMs; this.retries.delayMs += delayMs; } this.retries.count++; return this.fetchWithRetry(retryRequest, context); } async fetch(request) { this.sessions.setRequestSession(request); await this.signer.sign(request); const response = await this.systemFetch(request); this.sessions.readResponseSession(response); const requestCharge = +(response.headers.get('x-ms-request-charge') || 0); this.requestCharges += requestCharge; return response; } setHeaders(headers, args) { headers.set('accept', 'application/json'); headers.set('cache-control', 'no-cache'); headers.set('x-ms-version', '2018-12-31'); if (args.activityId) { headers.set('x-ms-activity-id', args.activityId); } const consistencyLevel = args.consistencyLevel || this.consistencyLevel; headers.set('x-ms-consistency-level', consistencyLevel); if (args.continuation) { headers.set('x-ms-continuation', args.continuation); } if (args.enableScan) { headers.set('x-ms-documentdb-query-enable-scan', 'true'); } if (args.ifMatch) { headers.set('if-match', args.ifMatch); } if (args.ifModifiedSince) { headers.set('if-modified-since', args.ifModifiedSince.toUTCString()); } if (args.ifNoneMatch) { headers.set('if-none-match', args.ifNoneMatch); } if (args.indexingDirective) { headers.set('x-ms-indexing-directive', args.indexingDirective); } if (args.isQuery) { headers.set('x-ms-documentdb-isquery', args.isQuery.toString()); } if (args.isUpsert) { headers.set('x-ms-documentdb-is-upsert', args.isUpsert.toString()); } if (args.maxItems) { headers.set('x-ms-max-item-count', args.maxItems.toString(10)); } if (args.offerType) { headers.set('x-ms-offer-type', args.offerType); } if (args.offerThroughput) { headers.set('x-ms-offer-throughput', args.offerThroughput.toString()); } if (args.populateMetrics) { headers.set('x-ms-documentdb-populatequerymetrics', 'true'); } if (args.partitionKey) { headers.set('x-ms-documentdb-partitionkey', escapeNonASCII(JSON.stringify([args.partitionKey]))); if (args.isQuery) { headers.set('x-ms-documentdb-query-enablecrosspartition', 'false'); } } else if (args.enableCrossPartition !== undefined && args.isQuery) { headers.set('x-ms-documentdb-query-enablecrosspartition', args.enableCrossPartition.toString()); } } } function toBodyInit(obj) { if ((typeof obj === 'string' || DataView.prototype.isPrototypeOf(obj), ArrayBuffer.isView(obj) || obj instanceof ReadableStream)) { return obj; } return JSON.stringify(obj); }