diff --git a/README.md b/README.md index aa60794..be9583f 100644 --- a/README.md +++ b/README.md @@ -6,24 +6,40 @@ Refer to the DocumentDB documentation here: https://docs.microsoft.com/en-us/azu **No TypeScript required** — you can use this module with plain JavaScript too (ES3, ES5, or ES6 aka ES2015 and whatever comes after), and enjoy enhanced Intellisense in an editor that supports TypeScript 2 definition files, such as VS Code. -> **NOTE:** The author of this module is not affiliated with Microsoft, Azure, or Document DB. +> **NOTE:** The author of this module is _not_ affiliated with Microsoft, Azure, or Document DB. ### Goals This module was written with the following goals in mind: - Streamline common DocumentDB use cases; - Enable a better developer experience with accurate Intellisense; -- Reduce clutter by adding classes and combining methods; +- Reduce clutter by grouping methods into classes and combining some of their functionality; - Use idiomatic TypeScript 2 (es6 for Node JS) internally and externally; - Enable asynchronous programming with `async/await` and/or Promises (native Node JS). -### Project Status -I just needed something quick, so at this point parts of the DocumentDB feature set are still missing. If your app needs stored procedures, or users and permissions, for example, then please add to this code (preferably as new classes). Pull requests are greatly appreciated! +### Change Log + +**v1.0.0:** + +* _Important:_ This version now requires TypeScript 2.1+. +* _New:_ Added an `existsAsync` method to `Collection` that uses a `select count(1) from c where...` query to determine more efficiently if any documents exist that match given ID or properties. +* _New:_ Added `path` properties to `Database` and `Collection`, which can be used with the underlying Node.js API (through `Client.documentClient`) if needed. +* _New:_ Most methods now accept an `options` parameter to forward feed and/or request options to the underlying Node.js API (e.g. for `enableCrossPartitionQuery`). +* _Improved:_ Where possible, document IDs are now used to locate document resources instead of mandatory `_self` links. This allows for a new overload of the `deleteDocumentAsync` method that just takes an ID, and removes the need for a query in `findDocumentAsync` if an ID is passed in (either as a property or as a single parameter). Also, `storeDocumentAsync` with `StoreMode.UpdateOnly` no longer requires a `_self` property, an `id` property will do. +* _Improved:_ More accurate types for objects passed to and/or returned from `Collection` methods. E.g. query results generated by `queryDocuments` no longer automatically include document properties such as `id` and `_self`, because queries may not actually return full documents anyway (or a document at all, e.g. for `select value...` queries). This is a breaking change since the TypeScript compiler may no longer find these properties on result objects, even for `select *` queries. The `findDocumentAsync` and `queryDocuments` methods now accept a type parameter to specify a result type explicitly. +* _Changed:_ Getting `Client.documentClient` now throws an exception if the client connection has not been opened yet, or has been closed. Use `isOpen()` to check if the connection is currently open. +* _Fixed:_ Operations are now queued properly in `DocumentStream`, e.g. calling `.read()` twice in succession (synchronously) actually returns promises for two different results. +* _Fixed:_ Added `strictNullChecks` and `noImplicitAny` to the TypeScript configuration for compatibility with projects that have these options enabled. +* _Fixed:_ Added TypeScript as a development dependency to `package.json`. + +**Note:** + +At this point parts of the DocumentDB feature set are still missing. If your app needs stored procedures, or users and permissions, for example, then please add to this code (preferably as new classes). Pull requests are greatly appreciated! Tests are sorely needed as well. Perhaps some of the tests can be ported over from DocumentDB itself. ## Installation -Use `npm` to install this module (TypeScript optional): +Use `npm` to install this module: ``` npm install documentdb-typescript @@ -171,29 +187,31 @@ async function main(url, masterKey) { // create a document (fails if ID exists), // returns document with meta properties - var doc: any = { id: "abc", foo: "bar" }; + var doc = { id: "abc", foo: "bar" }; doc = await coll.storeDocumentAsync(doc, StoreMode.CreateOnly); - // update a document (fails if not found), - // using _self property which must exist + // update a document (fails if not found) doc.foo = "baz"; doc = await coll.storeDocumentAsync(doc, StoreMode.UpdateOnly); // update a document if not changed in DB, - // using _etag property which must exist + // using _etag property (which must exist) doc.foo = "bla"; doc = await coll.storeDocumentAsync(doc, StoreMode.UpdateOnlyIfNoChange); - // upsert a document (twice, without errors) - var doc2: any = { id: "abc", foo: "bar" }; - var doc3: any = { id: "abc", foo: "baz" }; + // upsert a document (in parallel, without errors) + var doc2 = { id: "abc", foo: "bar" }; + var doc3 = { id: "abc", foo: "baz" }; await Promise.all([ coll.storeDocumentAsync(doc, StoreMode.Upsert), coll.storeDocumentAsync(doc) // same ]); - // delete the document, using _self property + // delete the document (fails if not found) await coll.deleteDocumentAsync(doc); + + // ... or delete by ID (fails if not found) + await coll.deleteDocumentAsync("abc"); } ``` @@ -206,10 +224,21 @@ async function main(url, masterKey) { var coll = await new Collection("test", "sample", url, masterKey) .openOrCreateDatabaseAsync(); - // find a document by ID (fails if not found) + // check if a document with given ID exists + // (uses "count(1)" aggregate in a query) + var exists = coll.existsAsync("abc"); + + // check if a document with given properties exists + // (exact match, also uses "count(1)" aggregate) + var customerExists = coll.existsAsync({ + isCustomer: true, + customerID: "1234" + }) + + // retrieve a document by ID (fails if not found) var doc = await coll.findDocumentAsync("abc"); - // find a document with given properties + // retrieve a document with given properties // (exact match, fails if not found, takes // newest if multiple documents match) try { @@ -218,7 +247,7 @@ async function main(url, masterKey) { isInactive: false, email: "foo@example.com" }); - console.log("Found " + user._self); + console.log(`Found ${user.email}: ${user.id}`); } catch (err) { console.log("User not found"); @@ -246,26 +275,26 @@ async function main(url, masterKey) { .openOrCreateDatabaseAsync(); // load all documents into an array - var q = coll.queryDocuments(); - var allDocs = await q.toArray(); + var allDocs = await coll.queryDocuments().toArray(); - // process all documents asynchronously - var q2 = await coll.queryDocuments("select * from c"); - q2.forEach(doc => { - console.log(doc._self); - }); - - // ... and the same, in a loop - q.reset(); + // read all results in a loop (with type hint) + type FooResult = { foo: string }; + var stream = coll.queryDocuments("select c.foo from c"); while (true) { - var rdoc = await q.read(); - if (!rdoc) break; - console.log(rdoc._self); + var it = await stream.next(); + if (it.done === true) break; + console.log(it.value.foo); } - - // map all documents asynchronously - var ids = await coll.queryDocuments("select * from c") - .mapAsync(doc => doc.id); + + // ... or use the forEach method + // (can be awaited, too) + await stream.reset().forEach(doc => { + console.log(doc.foo); + }); + + // ... or map all results to another array + var ids = await stream.mapAsync(doc => doc.id); + console.log(ids); // get only the newest time stamp var newest = await coll.queryDocuments( diff --git a/package.json b/package.json index cd4863d..5ceb060 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,13 @@ { "name": "documentdb-typescript", - "version": "0.1.1", + "version": "1.0.0", "description": "TypeScript API for Microsoft Azure DocumentDB", - "keywords": ["DocumentDB", "DocDB", "Azure", "TypeScript"], + "keywords": [ + "DocumentDB", + "DocDB", + "Azure", + "TypeScript" + ], "main": "./dist/index.js", "typings": "./typings/index.d.ts", "scripts": { @@ -21,5 +26,9 @@ "homepage": "https://github.com/jcormont/documentdb-typescript#readme", "dependencies": { "documentdb": "^1.10.0" + }, + "devDependencies": { + "@types/node": "^7.0.8", + "typescript": "^2.1.0" } } diff --git a/src/Client.ts b/src/Client.ts index 86b8541..e028bd1 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -1,13 +1,7 @@ -import { _DocumentDB } from "./_DocumentDB"; +import * as _DocumentDB from "./_DocumentDB"; import { curryPromise, sleepAsync } from "./Util"; import { Database } from "./Database"; -// get to the native DocumentDB client constructor -declare var require, process; -const documentdb = require("documentdb"); -const DocumentClient: _DocumentDB.DocumentClient_Ctor = - documentdb.DocumentClient; - /** List of opened/opening clients for specific endpoint/key combinations */ var _openClients = new Map(); @@ -16,7 +10,7 @@ var _uid = 1; /** Represents a DocumentDB endpoint */ export class Client { - constructor(url?: string, masterKey?: string) { + constructor(url?: string, masterKey = "") { this.url = url || ""; this.authenticationOptions = { masterKey }; } @@ -42,8 +36,15 @@ export class Client { /** The consistency level (string: "Strong" | "BoundedStaleness" | "Session" | "Eventual") used for the connection to this endpoint, if specified */ public consistencyLevel: _DocumentDB.ConsistencyLevel | undefined; - /** The native DocumentClient instance, if opened */ - public get documentClient() { return this._client }; + /** The native DocumentClient instance; throws an error if this client is currently not connected (check using .isOpen, or await .openAsync() first) */ + public get documentClient(): _DocumentDB.DocumentClient { + if (this._closed) throw new Error("Client already closed"); + if (!this._client) throw new Error("Document DB client is not connected"); + return this._client; + } + + /** Returns true if this client is currently connected through a native DocumentClient instance */ + public get isOpen() { return !!this._client && !this._closed } /** Connect to the endpoint represented by this client and validate the connection, unless already connected */ public openAsync(maxRetries = 3): PromiseLike { @@ -56,26 +57,26 @@ export class Client { JSON.stringify(this.connectionPolicy) + ":" + this.consistencyLevel; if (_openClients.has(key)) { - var other = _openClients.get(key); + var other = _openClients.get(key)!; this._client = other._client; this._databaseResources = other._databaseResources; - return this._open = other._open; + return this._open = other._open!; } _openClients.set(key, this); // create a new DocumentClient instance - this._client = new DocumentClient(this.url, + this._client = new _DocumentDB.DocumentClient(this.url, this.authenticationOptions, this.connectionPolicy, this.consistencyLevel); // return a promise that resolves when databases are read return this._open = new Promise(resolve => { - let tryConnect = (callback) => + let tryConnect = (callback: (err: any, result: any) => void) => this.log("Connecting to " + this.url) && - this.documentClient.readDatabases({ maxItemCount: 1000 }) + this._client!.readDatabases({ maxItemCount: 1000 }) .toArray(callback); resolve(curryPromise(tryConnect, this.timeout, maxRetries)() - .then(dbs => { this._resolve_databases(dbs) })); + .then(dbs => { this._resolve_databases!(dbs) })); }); } @@ -90,11 +91,11 @@ export class Client { }); // read all databases again and resolve promise - let tryReadDBs = (callback) => + let tryReadDBs = (callback: (err: any, result: any) => void) => this.log("Reading list of databases") && - this.documentClient.readDatabases({ maxItemCount: 1000 }) + this._client!.readDatabases({ maxItemCount: 1000 }) .toArray(callback); - this._resolve_databases( + this._resolve_databases!( await curryPromise(tryReadDBs, this.timeout, maxRetries)()); } var databaseResources = await this._databaseResources; @@ -103,12 +104,13 @@ export class Client { } /** @internal Create a database (and add it to the list returned by listDatabasesAsync) */ - public async createDatabaseAsync(id: string) { + public async createDatabaseAsync(id: string, maxRetries?: number, + options?: _DocumentDB.RequestOptions) { await this.openAsync(); - let tryCreateDB = (callback) => + let tryCreateDB = (callback: (err: any, result: any) => void) => this.log("Creating database: " + id) && - this._client.createDatabase({ id }, undefined, callback); - await curryPromise(tryCreateDB, this.timeout)(); + this._client!.createDatabase({ id }, options, callback); + await curryPromise(tryCreateDB, this.timeout, maxRetries)(); // reload all database resources until the created DB appears // (this is to allow for consistency less than session consistency) @@ -123,14 +125,14 @@ export class Client { /** Get account information */ public async getAccountInfoAsync() { - let tryGetInfo = (callback) => + let tryGetInfo = (callback: (err: any, result: any) => void) => this.log("Getting account info") && - this.documentClient.getDatabaseAccount(callback); + this._client!.getDatabaseAccount(callback); return <_DocumentDB.DatabaseAccount>await curryPromise( tryGetInfo, this.timeout)(); } - /** Remove the current connection; an attempt to open the same endpoint again in another instance will open and validate the connection again */ + /** Remove the current connection; an attempt to open the same endpoint again in another instance will open and validate the connection again, but the current instance cannot be re-opened */ public close() { this._closed = true; _openClients.forEach((client, key) => { @@ -139,29 +141,29 @@ export class Client { } /** @internal Log a message; always returns true */ - public log(message) { + public log(message: string): true { if (this.enableConsoleLog) console.log(`[${process.pid}]{${this._uid}} ${Date.now()} ${message}`); return true; } /** @internal List of databases found in the account, resolved if and when opened */ - private _databaseResources = new Promise<_DocumentDB.Resource[]>(resolve => { + private _databaseResources = new Promise<_DocumentDB.DatabaseResource[]>(resolve => { this._resolve_databases = resolve; }); /** @internal */ - private _resolve_databases: (data) => void; + private _resolve_databases?: (data: any) => void; /** @internal */ - private _open: PromiseLike; + private _open?: PromiseLike; /** @internal */ - private _client: _DocumentDB.DocumentClient | null = null; + private _client?: _DocumentDB.DocumentClient; /** @internal */ - private _closed: boolean; + private _closed?: boolean; /** @internal */ private _uid = _uid++; -} \ No newline at end of file +} diff --git a/src/Collection.ts b/src/Collection.ts index e78a484..16c2f18 100644 --- a/src/Collection.ts +++ b/src/Collection.ts @@ -1,9 +1,18 @@ -import { _DocumentDB } from "./_DocumentDB"; +import * as _DocumentDB from "./_DocumentDB"; import { Database } from "./Database"; import { Client } from "./Client"; import { DocumentStream } from "./DocumentStream"; import { curryPromise, sleepAsync } from "./Util"; +/** Global query ID, used to tag reads in the log */ +var _queryUID = 0; + +/** Modes that can be used for storing resources in a collection */ +export enum StoreMode { Upsert, CreateOnly, UpdateOnly, UpdateOnlyIfNoChange }; + +/** Combined option objects for operations that may invoke multiple network calls */ +export type AllOptions = _DocumentDB.FeedOptions & _DocumentDB.RequestOptions; + /** Represents a DocumentDB collection */ export class Collection { /** Refer to a collection by name, from given database */ @@ -34,16 +43,22 @@ export class Collection { /** The database used for all operations */ public readonly database: Database; + /** The partial resource URI for this database, i.e. `"/dbs/.../colls/..."` */ + public get path() { + return this._self || + "dbs/" + this.database.id + "/colls/" + encodeURIComponent(this.id); + } + /** Open and validate the connection, check that this collection exists */ - public async openAsync(maxRetries?: number) { + public async openAsync(maxRetries?: number, options?: AllOptions) { if (this._self) return this; await this.database.openAsync(maxRetries); - let tryGetCollection = (callback) => - this.database.client.log("Reading collection " + this.id) && + let tryGetCollection = (callback: (err: any, result: any) => void) => + this.database.client.log("Reading collection " + this.path) && this.database.client.documentClient.readCollection( - "dbs/" + this.database.id + "/colls/" + this.id, - undefined, callback); - var resource = await curryPromise(tryGetCollection, + this.path, options, callback); + var resource = await curryPromise<_DocumentDB.CollectionResource>( + tryGetCollection, this.database.client.timeout, maxRetries)(); this._self = resource["_self"]; return this; @@ -52,33 +67,34 @@ export class Collection { /** Open and validate the connection, find or create collection resource (does not create the database) */ public async openOrCreateAsync(createThroughput?: number, indexingPolicy?: _DocumentDB.IndexingPolicy, - defaultTtl?: number, maxRetries?: number) { + defaultTtl?: number, maxRetries?: number, options?: AllOptions) { if (this._self) return this; await this.database.openAsync(maxRetries); - var resource: {}; + var resource: _DocumentDB.CollectionResource; try { - let tryGetCollection = (callback) => + let tryGetCollection = (callback: (err: any, result: any) => void) => this.database.client.log("Reading collection " + this.id) && this.database.client.documentClient.readCollection( - "dbs/" + this.database.id + "/colls/" + this.id, - undefined, callback); - resource = await curryPromise(tryGetCollection, + this.path, options, callback); + resource = await curryPromise<_DocumentDB.CollectionResource>( + tryGetCollection, this.database.client.timeout, maxRetries)(); } catch (err) { if (err.code == 404 /* not found */) { // create the collection now - var data: _DocumentDB.CollectionResource = { id: this.id }; + var data: any = { id: this.id }; if (indexingPolicy) data.indexingPolicy = indexingPolicy; if (defaultTtl !== undefined) data.defaultTtl = defaultTtl; try { - let tryCreateCollection = (callback) => + let tryCreateCollection = (callback: (err: any, result: any) => void) => this.database.client.log("Creating collection " + this.id) && this.database.client.documentClient.createCollection( - "dbs/" + this.database.id, data, + this.database.path, data, { offerThroughput: createThroughput }, callback); - resource = await curryPromise(tryCreateCollection, + resource = await curryPromise<_DocumentDB.CollectionResource>( + tryCreateCollection, this.database.client.timeout)(); } catch (err) { @@ -108,169 +124,299 @@ export class Collection { } /** Get offer (throughput provisioning) information */ - public async getOfferInfoAsync() { + public async getOfferInfoAsync(maxRetries?: number, options?: AllOptions) { await this.openAsync(); - let tryGetOffer = (callback) => + let tryGetOffer = (callback: (err: any, result: any) => void) => this.database.client.log("Getting offer info for " + this.id) && this.database.client.documentClient.queryOffers({ query: "select * from root r where r.resource = @selflink", parameters: [{ name: "@selflink", value: this._self }] - }).toArray(callback); - var offers: _DocumentDB.OfferResource[] = - await curryPromise(tryGetOffer, this.database.client.timeout)(); + }, options).toArray(callback); + var offers: any[] = await curryPromise( + tryGetOffer, this.database.client.timeout, maxRetries)(); if (!offers.length) throw new Error("Offer not found"); this._offer = offers[0]; - return JSON.parse(JSON.stringify(offers[0])); + return <_DocumentDB.OfferResource>JSON.parse(JSON.stringify(offers[0])); } /** Set provisioned throughput */ public async setOfferInfoAsync(throughput: number) { await this.openAsync(); if (!this._offer) await this.getOfferInfoAsync(); - if (!this._offer.content || !this._offer.content.offerThroughput) + var offer = this._offer!; + if (!offer.content || !offer.content.offerThroughput) throw new Error("Unknown offer type"); - this._offer.content.offerThroughput = throughput; - let trySetOffer = (callback) => + offer.content.offerThroughput = throughput; + let trySetOffer = (callback: (err: any, result: any) => void) => this.database.client.log("Setting offer info for " + this.id) && - this.database.client.documentClient.replaceOffer(this._offer._self, - this._offer, callback); - this._offer = await curryPromise(trySetOffer, this.database.client.timeout)(); + this.database.client.documentClient.replaceOffer( + offer._self, offer, callback); + this._offer = await curryPromise(trySetOffer, + this.database.client.timeout)(); } /** Delete this collection */ - public async deleteAsync() { + public async deleteAsync(maxRetries?: number, options?: AllOptions) { await this.openAsync(); - let tryDelete = (callback) => + let tryDelete = (callback: (err: any, result: any) => void) => this.database.client.log("Deleting collection: " + this.id) && - this.database.client.documentClient.deleteCollection(this._self, - undefined, callback); - await curryPromise(tryDelete, this.database.client.timeout)(); + this.database.client.documentClient.deleteCollection(this._self!, + options, callback); + await curryPromise(tryDelete, this.database.client.timeout, + maxRetries, 500, true)(); delete this._self; } - /** Create or update the document with given data; returns the stored data as a plain object, including meta properties such as `_etag` and `_self` */ - public async storeDocumentAsync(data: _DocumentDB.DocumentResource, - mode?: StoreMode, maxRetries?: number): - Promise<_DocumentDB.ReadDocumentResource> { + /** Create or update the document with given data (must include an `.id` or `._self` property if store mode is `UpdateOnly`, and must also include an `_etag` property if store mode is `UpdateOnlyIfNoChange`); returns the stored data as a plain object, including meta properties such as `._etag` and `._self` */ + public async storeDocumentAsync>( + data: T, + mode?: StoreMode, maxRetries?: number, options?: AllOptions): + Promise { await this.openAsync(); - if (!data) throw new TypeError(); - var tryStore: (callback) => any, options: _DocumentDB.RequestOptions; + if (!(data instanceof Object)) throw new TypeError(); + var tryStore: (callback: (err: any, result: any) => void) => any; switch (mode) { case StoreMode.UpdateOnlyIfNoChange: if (!data._etag) throw new Error("Document _etag missing"); - options = { + options = Object.assign({ accessCondition: { type: "IfMatch", condition: data._etag } - }; + }, options); // continue with update... case StoreMode.UpdateOnly: - if (!data._self) throw new Error("Document _self missing"); + if (data.id === undefined) throw new Error("Document ID missing"); tryStore = (callback) => this.database.client.log("Replacing document: " + data.id) && - this.database.client.documentClient.replaceDocument(data._self, - data, options, callback); + this.database.client.documentClient.replaceDocument( + this._getDocURI(data), + data, options, callback); break; case StoreMode.CreateOnly: tryStore = (callback) => this.database.client.log("Creating document: " + (data.id !== undefined && data.id !== "" ? data.id : "(auto generated ID)")) && - this.database.client.documentClient.createDocument(this._self, - data, undefined, callback); + this.database.client.documentClient.createDocument(this._self!, + data, options, callback); break; default: tryStore = (callback) => this.database.client.log("Upserting document: " + (data.id !== undefined && data.id !== "" ? data.id : "(auto generated ID)")) && - this.database.client.documentClient.upsertDocument(this._self, - data, undefined, callback); + this.database.client.documentClient.upsertDocument(this._self!, + data, options, callback); } - return <_DocumentDB.ReadDocumentResource> - await curryPromise(tryStore, this.database.client.timeout, maxRetries)(); + return await curryPromise( + tryStore, this.database.client.timeout, maxRetries)(); } /** Find the document with given ID */ - public async findDocumentAsync(id: string, maxRetries?: number): - Promise<_DocumentDB.ReadDocumentResource>; - /** Find the document with the same `id` property, or the first document with exactly the given properties if `id` is not set */ - public async findDocumentAsync(properties: {}, maxRetries?: number): Promise<_DocumentDB.ReadDocumentResource>; - public async findDocumentAsync(obj: string | {}, maxRetries?: number) { + public async findDocumentAsync(id: string, + maxRetries?: number, options?: AllOptions): + Promise; + /** Reload given document from the database using its `._self` property */ + public async findDocumentAsync(doc: { _self: string }, + maxRetries?: number, options?: AllOptions): + Promise; + /** Find the document with the same `id` property as given object, or the latest document with exactly the same values for all given properties if `id` is not set */ + public async findDocumentAsync(obj: Partial, + maxRetries?: number, options?: AllOptions): + Promise; + public async findDocumentAsync( + obj: string | { id?: string, _self?: string, [p: string]: any }, + maxRetries?: number, options?: any) { + await this.openAsync(); + + // read using readDocument if possible + if (typeof obj === "string" || + obj && (typeof (obj)._self === "string" || + (typeof (obj).id === "string"))) { + var docURI: string | undefined; + try { docURI = this._getDocURI(obj) } catch (all) { } + if (docURI) { + // if got a well-formed URI, go ahead (and retry on 404, for + // lower consistency modes) + let tryReload = (callback: (err: any, result: any) => void) => + this.database.client.log("Reading document " + docURI) && + this.database.client.documentClient.readDocument( + docURI!, options, callback); + return await curryPromise(tryReload, + this.database.client.timeout, maxRetries, undefined, true)(); + } + else if (typeof obj === "string") { + // select by ID property (e.g. when contains spaces) + obj = { id: obj }; + } + } + + // use queryDocuments with given query/properties var q: _DocumentDB.SqlQuery = { query: "select top 1 * from c where", parameters: [] }; + if (obj instanceof Object) { + for (var prop in <{}>obj) { + if (Object.prototype.hasOwnProperty.call(obj, prop)) { + // add an exact match for this property + if (q.parameters.length) q.query += " and"; + q.query += ` c.${prop} = @_${prop}_value`; + q.parameters.push({ + name: "@_" + prop + "_value", + value: obj[prop] + }); + } + } + } + if (!q.parameters.length) q.query += " true"; + + // sort by time stamp to get latest document first + q.query += " order by c._ts desc"; + + // return a single resource + let tryQuery = (callback: (err: any, result: any) => void) => + this.database.client.log("Querying collection " + this.id + ": " + + JSON.stringify(q)) && + this.database.client.documentClient.queryDocuments( + this._self!, q, options).toArray(callback); + var results = await curryPromise(tryQuery, + this.database.client.timeout, maxRetries)(); + if (!results || !results.length) + throw new Error("Resource not found"); + return results[0]; + } + + /** Check if a document with given ID exists (without reading the document) */ + public async existsAsync(id: string, + maxRetries?: number, options?: AllOptions): Promise; + /** Check if a document with given properties exists (i.e. where _all_ own properties of the given object match exactly) */ + public async existsAsync(obj: {}, + maxRetries?: number, options?: AllOptions): Promise; + public async existsAsync( + obj: string | { id?: string, _self?: string, [p: string]: any }, + maxRetries?: number, options?: any) { + await this.openAsync(); + + // use queryDocuments with given ID or properties + var q: _DocumentDB.SqlQuery = { + query: "select value count(1) from c where", + parameters: [] + }; if (typeof obj === "string") { - // match single ID q.query += " c.id = @id"; q.parameters.push({ name: "@id", value: obj }); } - else if (obj["id"]) { - // match single ID from object (discard other properties) - q.query += " c.id = @id"; - q.parameters.push({ name: "@id", value: obj["id"] }); - } - else { - if (obj instanceof Object) { - for (var prop in <{}>obj) { - if (Object.prototype.hasOwnProperty.call(obj, prop)) { - // add an exact match for this property - if (q.parameters.length) q.query += " and"; - q.query += ` c.${prop} = @_${prop}_value`; - q.parameters.push({ - name: "@_" + prop + "_value", - value: obj[prop] - }); - } + else if (obj instanceof Object) { + for (var prop in <{}>obj) { + if (Object.prototype.hasOwnProperty.call(obj, prop)) { + // add an exact match for this property + if (q.parameters.length) q.query += " and"; + q.query += ` c.${prop} = @_${prop}_value`; + q.parameters.push({ + name: "@_" + prop + "_value", + value: obj[prop] + }); } } - if (!q.parameters.length) q.query += " true"; - q.query += " order by c._ts desc"; } + if (!q.parameters.length) q.query += " true"; - // return a single resource - await this.openAsync(); - let tryQuery = (callback) => + // run the query and return true only if count >= 1 + let tryQuery = (callback: (err: any, result: any) => void) => this.database.client.log("Querying collection " + this.id + ": " + JSON.stringify(q)) && - this.database.client.documentClient.queryDocuments(this._self, q) + this.database.client.documentClient + .queryDocuments(this._self!, q, options) .toArray(callback); - var results: _DocumentDB.DocumentResource[] = - await curryPromise(tryQuery, this.database.client.timeout, maxRetries)(); - if (!results || !results.length) - throw new Error("Resource not found"); - return results[0]; + var results = await curryPromise(tryQuery, + this.database.client.timeout, maxRetries)(); + return !!results && results[0] >= 1; } - /** Query documents in this collection using a SQL query string, or SQL query object (in the form `{ query: "...", parameters: [{ name: "@...", value: ... }, ...] }`), defaults to "select * from c"; returns an encapsulation of the query results that can be used to asynchronously load all results or process them one by one */ - public queryDocuments(query: _DocumentDB.SqlQuery = "select * from c", - batchSize?: number) { - if (!this._self) throw new Error("Collection is not open yet"); - var options: _DocumentDB.FeedOptions; - if (batchSize) options = { maxItemCount: batchSize }; - return new DocumentStream(this, - this.database.client.documentClient.queryDocuments(this._self, - query, options)); + /** Query documents in this collection using a SQL query string, or SQL query object (i.e. `{ query: "...", parameters: [{ name: "@...", value: ... }, ...] }`) */ + public queryDocuments(query: _DocumentDB.SqlQuery, batchSize?: number): + DocumentStream; + /** Query documents in this collection using a SQL query string, or SQL query object (i.e. `{ query: "...", parameters: [{ name: "@...", value: ... }, ...] }`) */ + public queryDocuments(query: _DocumentDB.SqlQuery, options?: AllOptions): + DocumentStream; + /** Query all documents in this collection */ + public queryDocuments(query?: undefined, batchSize?: number): + DocumentStream; + /** Query all documents in this collection */ + public queryDocuments(query?: undefined, options?: AllOptions): + DocumentStream; + public queryDocuments(query?: _DocumentDB.SqlQuery, options?: number | AllOptions) { + if (typeof options === "number") options = { maxItemCount: options }; + var uid = ++_queryUID; + if (query === undefined) { + // use readDocuments to get all documents + return DocumentStream.create(this, uid, this.openAsync().then(() => + this.database.client.log( + `[${uid}>>] Reading all documents from ${this.id}`) && + this.database.client.documentClient.readDocuments( + this._self!, options))); + } + else { + // submit given query + return DocumentStream.create(this, uid, this.openAsync().then(() => + this.database.client.log( + `[${uid}>>] Querying collection ${this.id}: ` + + JSON.stringify(query)) && + this.database.client.documentClient.queryDocuments( + this._self!, query, options))); + } } - /** Delete the given document (must have a _self property, e.g. an object passed to `storeDocumentAsync`, or the result of `findDocumentAsync`) */ - public async deleteDocumentAsync(data: { id?: string, _self: string }, maxRetries?: number) { + /** Delete the document with given ID */ + public async deleteDocumentAsync(id: string, + maxRetries?: number, options?: AllOptions): Promise; + /** Delete the given document (must have a `_self` property, e.g. the result of `storeDocumentAsync` or `findDocumentAsync`, OR a valid `id` property) */ + public async deleteDocumentAsync(doc: { _self: string } | { id: string }, + maxRetries?: number, options?: AllOptions): Promise; + public async deleteDocumentAsync(v: string | { id?: string, _self?: string }, + maxRetries?: number, options?: AllOptions) { await this.openAsync(); - let tryDelete = (callback) => - this.database.client.log("Deleting: " + (data.id || data._self)) && - this.database.client.documentClient.deleteDocument(data._self, - undefined, callback); - await curryPromise(tryDelete, this.database.client.timeout, maxRetries)(); + var id = typeof v === "string" ? v : v.id; + var docURI: string | undefined; + try { docURI = this._getDocURI(v) } catch (all) { } + if (!docURI) { + // ID may contain invalid characters, find _self instead + var obj = await this.queryDocuments<{ _self: string }>({ + query: "select c._self from c where c.id = @id", + parameters: [{ name: "@id", value: id }] + }, options).read(); + if (!obj) throw new Error("Resource not found"); + docURI = obj._self; + } + + // use deleteDocument to delete by URI (retry on 404 a few times) + let tryDelete = (callback: (err: any, result: any) => void) => + this.database.client.log("Deleting: " + (id || "")) && + this.database.client.documentClient.deleteDocument( + docURI!, options, callback); + await curryPromise(tryDelete, this.database.client.timeout, + maxRetries, 500, true)(); + } + + /** @internal Helper function that returns a document URI for given ID or object */ + private _getDocURI(v: string | { id?: string, _self?: string }): string { + if (typeof v !== "string") { + if (v._self) return v._self; + v = String(v.id || ""); + } + var chars = /[\/\\\?#]/; + if (!v || chars.test(v) || chars.test(this.id)) + throw new Error("Invalid resource ID: " + JSON.stringify(v)); + return "dbs/" + this.database.id + + "/colls/" + this.id + + "/docs/" + v; } /** @internal Self link */ - private _self: string; + private _self?: string; /** @internal Offer link, if known */ - private _offer: _DocumentDB.OfferResource; + private _offer?: _DocumentDB.OfferResource; } - -/** Modes that can be used for storing resources in a collection */ -export enum StoreMode { Upsert, CreateOnly, UpdateOnly, UpdateOnlyIfNoChange }; diff --git a/src/Database.ts b/src/Database.ts index b3debd0..ea68542 100644 --- a/src/Database.ts +++ b/src/Database.ts @@ -1,3 +1,4 @@ +import * as _DocumentDB from "./_DocumentDB"; import { Client } from "./Client"; import { curryPromise, sleepAsync } from "./Util"; import { Collection } from "./Collection"; @@ -30,15 +31,20 @@ export class Database { /** The client used for all operations */ public readonly client: Client; + /** The partial resource URI for this database, i.e. `"/dbs/..."` */ + public get path() { + return this._self || "dbs/" + encodeURIComponent(this.id) + "/"; + } + /** Open and validate the connection, check that this database exists */ public async openAsync(maxRetries?: number) { if (this._self) return this; await this.client.openAsync(maxRetries); // find this database's self link from client's list of databases - var dbs = await this.client.listDatabasesAsync(); + var dbs = await this.client.listDatabasesAsync(false, maxRetries); dbs.some(r => (r.id === this.id ? !!(this._self = r._self) : false)); - + if (!this._self) throw new Error("Database does not exist: " + this.id); return this; } @@ -72,28 +78,32 @@ export class Database { } /** Get a list of Collection instances for this database */ - public async listCollectionsAsync() { - await this.openAsync(); - let tryListAll = (callback) => + public async listCollectionsAsync(maxRetries?: number, options?: _DocumentDB.FeedOptions) { + await this.openAsync(maxRetries); + + // get all collections using readCollections + let tryListAll = (callback: (err: any, result: any) => void) => this.client.log("Reading collections in " + this.id) && - this.client.documentClient.readCollections(this._self) + this.client.documentClient.readCollections(this._self!, options) .toArray(callback); - var resources = await curryPromise(tryListAll, this.client.timeout)(); - return (resources).map(r => - new Collection(r.id, this, r._self)); + var resources = await curryPromise(tryListAll, this.client.timeout, maxRetries)(); + + // map resources to Collection instances + return (resources).map(r => new Collection(r.id, this, r._self)); } /** Delete this database */ - public async deleteAsync() { - await this.openAsync(); - let tryDelete = (callback) => + public async deleteAsync(maxRetries?: number, options?: _DocumentDB.RequestOptions) { + await this.openAsync(maxRetries); + + // use deleteDatabase to delete the database (duh...) + let tryDelete = (callback: (err: any, result: any) => void) => this.client.log("Deleting database: " + this.id) && - this.client.documentClient.deleteDatabase(this._self, - undefined, callback); + this.client.documentClient.deleteDatabase(this._self!, options, callback); await curryPromise(tryDelete, this.client.timeout)(); delete this._self; } /** @internal Self link */ - private _self: string; + private _self?: string; } diff --git a/src/DocumentStream.ts b/src/DocumentStream.ts index 95a3f71..a315f64 100644 --- a/src/DocumentStream.ts +++ b/src/DocumentStream.ts @@ -1,54 +1,100 @@ -import { _DocumentDB } from "./_DocumentDB"; +import * as _DocumentDB from "./_DocumentDB"; import { Collection } from "./Collection"; import { curryPromise } from "./Util"; -/** Represents asynchronously loaded document sets as a stream */ -export class DocumentStream { - /** @internal constructor */ - constructor(private _collection: Collection, - private _qi: _DocumentDB.QueryIterator<_DocumentDB.DocumentResource>) { +export interface AsyncIterator { + next(value?: any): Promise>; + return?(value?: any): Promise>; + throw?(e?: any): Promise>; +} + +/** Represents asynchronously loaded query result sets as a stream; the type parameter represents the query result type, i.e. a full document resource type for `SELECT * FROM` queries, an object with only projected properties for `SELECT x, y, ... FROM` queries, or even a scalar value for `SELECT VALUE ... FROM` queries */ +export class DocumentStream implements AsyncIterator { + /** @internal create a document stream from a query iterator promise */ + public static create(_collection: Collection, _uid: number, + _qiP: Promise<_DocumentDB.QueryIterator>) { + return new DocumentStream(_collection, _uid, _qiP); + } + + /** Private constructor */ + private constructor(private _collection: Collection, private _uid: number, + private _qiP: Promise<_DocumentDB.QueryIterator>) { // nothing here } /** Timeout (ms) used for all operations; set to the Client timeout initially, set this to a large number if reading a large result set using `toArray` */ public timeout = this._collection.database.client.timeout; - /** Get the next document (asynchronously), if any; promise resolves to null if there are no documents left in the set */ - public async read() { - this._collection.database.client.log("Reading from stream..."); - var next: _DocumentDB.DocumentResource = - await curryPromise(this._qi.nextItem.bind(this._qi), - this.timeout, 0)(); - return next || null; + /** Get the next result (asynchronously), if any; promise resolves to the result, or to `null` if there are no results left in the set, or is rejected if an error occurred; subsequent calls to this function will return promises for results after the current result (i.e. requests are queued) */ + public async read(): Promise { + var nextResult = await this.next(); + return nextResult.done ? null : nextResult.value!; } - /** Call a function for each document, until all documents have been processed or the callback returns `false` or throws an error */ - public forEach(f: (doc: _DocumentDB.DocumentResource) => any) { - let next = () => this.read().then(doc => { - if (!doc) return; - if (f(doc) !== false) next(); - }); - next(); + // TODO: the following method can be used to turn this object into an ES6 + // asynchronous iterator, when that feature lands in TypeScript 2.3 (?), + // i.e. we just need [Symbol.asyncIterator] = () => this + /** Get the next result (asynchronously), if any; promise resolves to a `{ value, done }` pair, or is rejected if an error occurred; subsequent calls to this function will return promises for results after the current result (i.e. requests are queued) */ + public async next(): Promise<{ value: T, done: false} | { value: any, done: true}> { + var qi = this._qi || (this._qi = await this._qiP); + var readNextAsync = curryPromise(qi.nextItem.bind(qi), this.timeout, 0, 100); + var next: T = await (this._nextP = this._nextP.then(() => + this._collection.database.client.log( + `[>>${this._uid}] Reading from stream...`) && + readNextAsync())); + return next !== undefined ? + { value: next, done: false } : + { value: undefined, done: true }; + } + + /** Call a function for each result, until all results have been processed or the callback returns `false` or throws an error; returned promise resolves to true if all results have been processed, or false otherwise, or is rejected if an error occurred */ + public forEach(f: (doc: T) => any) { + let next = (): PromiseLike => { + return this.next().then(n => { + if (n.done) return true; + if (f(n.value) === false) return false; + return next(); + }); + }; + return next(); } - /** Call a function for each document; returns a promise for an array with all return values, which is resolved only when all documents have been processed, or is rejected if the callback throws an error */ - public async mapAsync(f: (doc: _DocumentDB.DocumentResource) => void) { + /** Call a function for each result; returns a promise for an array with all return values, which is resolved only when all results have been processed, or is rejected if the callback throws an error */ + public async mapAsync(f: (doc: T) => void) { var result = []; while (true) { - var doc = await this.read(); - if (!doc) return result; - result.push(f(doc)); + var n = await this.next(); + if (n.done) return result; + result.push(f(n.value)); } } - /** Reset the stream to the beginning of the set */ - public reset() { - this._qi.reset(); + /** Reset the stream to the beginning of the set (synchronously); returns the stream itself */ + public reset(): this { + this._qi && this._qi.reset(); + return this; } - /** Load all documents into an array */ + /** Reset the stream to the beginning of the set (asynchronously, i.e. after all queued operations have completed) */ + public resetAsync() { + this._nextP.then(() => { + this._qi && this._qi.reset(); + }); + } + + /** Load all results into an array */ public async toArray() { - return <_DocumentDB.DocumentResource[]>await curryPromise( - this._qi.toArray.bind(this._qi), this.timeout, 0)(); + var qi = this._qi || (this._qi = await this._qiP); + var readArrayAsync = curryPromise(qi.toArray.bind(qi), this.timeout, 0); + return await (this._nextP = this._nextP.then(() => + this._collection.database.client.log( + `[>>${this._uid}] Reading into array from stream...`) && + readArrayAsync())); } -} \ No newline at end of file + + /** @internal The resolved query iterator, if any */ + private _qi?: _DocumentDB.QueryIterator; + + /** @internal Promise for the last operation's result */ + private _nextP: PromiseLike = Promise.resolve(true); +} diff --git a/src/Util.ts b/src/Util.ts index b279667..6f2a0b6 100644 --- a/src/Util.ts +++ b/src/Util.ts @@ -4,11 +4,12 @@ import { Client } from "./Client"; var _pending = 0; /** Return a curried version of given function that appends a callback as a last parameter, which rejects or resolves a promise; the promise is returned immediately; also handles DocumentDB errors when possible */ -export function curryPromise(f: Function, timeout = 60000, maxRetries = 0, retryTimer?: number) { +export function curryPromise(f: Function, timeout = 60000, + maxRetries = 0, retryTimer?: number, retryOn404?: boolean) { return (...args: any[]): PromiseLike => { // return Promise for result or error var started = false, done: any; - var retries = maxRetries, timeoutTimer: number; + var retries = maxRetries, timeoutTimer: any; return new Promise(function exec(resolve, reject) { if (done) return; if (!started) { @@ -33,33 +34,37 @@ export function curryPromise(f: Function, timeout = 60000, maxRetries = 0, re } function clearTimeoutTimer() { clearTimeout(timeoutTimer); - timeoutTimer = -1; + timeoutTimer = undefined; } var retried = false; - function retry() { + function retry(err?: any, headers?: any) { if (retried) return; retried = true; clearTimeoutTimer(); + var t = retryTimer || 100; + if (err && err.code === 429 && + headers && headers["x-ms-retry-after-ms"]) + t = parseFloat(headers["x-ms-retry-after-ms"]); setTimeout(exec, retryTimer || 100, resolve, reject); } setTimeoutTimer(); // append own callback - args.push((err, result) => { + args.push((err: any, result: any, headers?: any) => { if (err) { // retry or reject - if (err.code != 400 && err.code != 401 && - err.code != 403 && err.code != 404 && - err.code != 409 && err.code != 412 && - err.code != 413 && retries-- > 0) { - retry(); + if (err.code !== 400 && err.code !== 401 && + err.code !== 403 && (retryOn404 || err.code !== 404) && + err.code !== 409 && err.code !== 412 && + err.code !== 413 && retries-- > 0) { + retry(err, headers); } else { - var error: Error, body: any; + var error: Error | undefined, body: any; try { body = JSON.parse(err.body) } catch (all) { } if (body) { error = new Error(body.message || "Database error"); - error["code"] = err.code; + (error).code = err.code; error.name = body.code; } reject(error || err); diff --git a/src/_DocumentDB.ts b/src/_DocumentDB.ts index 805297b..dbe5d1f 100644 --- a/src/_DocumentDB.ts +++ b/src/_DocumentDB.ts @@ -1,254 +1,313 @@ -export namespace _DocumentDB { - /** A native DocumentClient instance */ - export interface DocumentClient { - createAttachment; - createAttachmentAndUploadMedia; - createCollection(dbLink: string, body: CollectionResource, options: RequestOptions, callback: Callback); - createDatabase(body: Resource, options: RequestOptions, callback: Callback); - createDocument(collectionLink: string, body: DocumentResource, options: RequestOptions, callback: Callback); - createPermission(userLink: string, body: PermissionResource, options: RequestOptions, callback: Callback); - createStoredProcedure(collectionLink: string, sproc: SprocResource, options: RequestOptions, callback: Callback); - createTrigger; - createUser(dbLink: string, body: UserResource, options: RequestOptions, callback: Callback); - createUserDefinedFunction; - deleteAttachment; - deleteCollection(collectionLink: string, options: RequestOptions, callback: Callback); - deleteConflict; - deleteDatabase(dbLink: string, options: RequestOptions, callback: Callback); - deleteDocument(documentLink: string, options: RequestOptions, callback: Callback); - deletePermission(permissionLink: string, options: RequestOptions, callback: Callback); - deleteStoredProcedure(sprocLink: string, options: RequestOptions, callback: Callback); - deleteTrigger; - deleteUser(userLink: string, options: RequestOptions, callback: Callback); - deleteUserDefinedFunction; - executeStoredProcedure(sprocLink: string, params: any[], options: RequestOptions, callback: Callback); - getDatabaseAccount(callback: (error: ClientError, databaseAccount: DatabaseAccount) => void); - getReadEndpoint(callback: (string) => void); - getWriteEndpoint(callback: (string) => void); - queryAttachments; - queryCollections(dbLink: string, query: SqlQuery, options?: FeedOptions): QueryIterator; - queryConflicts(collectionLink: string, query: SqlQuery, options?: FeedOptions): QueryIterator; - queryDatabases(query: SqlQuery, options?: FeedOptions): QueryIterator; - queryDocuments(collectionLink: string, query: SqlQuery, options?: FeedOptions): QueryIterator; - queryOffers(query: SqlQuery, options?: FeedOptions): QueryIterator; - queryPermissions(userLink: string, query: SqlQuery, options?: FeedOptions): QueryIterator; - queryStoredProcedures(collectionLink: string, query: SqlQuery, options?: FeedOptions): QueryIterator; - queryTriggers; - queryUserDefinedFunctions; - queryUsers(dbLink: string, query: SqlQuery, options?: FeedOptions): QueryIterator; - readAttachment; - readAttachments; - readCollection(collectionLink: string, options: RequestOptions, callback: Callback); - readCollections(dbLink: string, options?: FeedOptions): QueryIterator; - readConflict(conflictLink: string, options: RequestOptions, callback: Callback); - readConflicts(collectionLink: string, options?: FeedOptions): QueryIterator; - readDatabase(dbLink: string, options: RequestOptions, callback: Callback); - readDatabases(options?: FeedOptions): QueryIterator; - readDocument(documentLink: string, options: RequestOptions, callback: Callback); - readDocuments(collectionLink: string, options?: FeedOptions): QueryIterator; - readMedia; - readOffer(offerLink: string, callback: Callback); - readOffers(options?: FeedOptions): QueryIterator; - readPermission(permissionLink: string, options: RequestOptions, callback: Callback); - readPermissions(userLink: string, options?: FeedOptions): QueryIterator; - readStoredProcedure(sprocLink: string, options: RequestOptions, callback: Callback); - readStoredProcedures(collectionLink: string, options?: FeedOptions): QueryIterator; - readTrigger; - readTriggers; - readUser(userLink: string, options: RequestOptions, callback: Callback); - readUsers(dbLink: string, options?: FeedOptions): QueryIterator; - readUserDefinedFunction; - readUserDefinedFunctions; - replaceAttachment; - replaceCollection; - replaceDocument(documentLink: string, document: DocumentResource, options: RequestOptions, callback: Callback); - replaceOffer(offerLink: string, offer: OfferResource, callback: Callback); - replacePermission(permissionLink: string, permission: PermissionResource, options: RequestOptions, callback: Callback); - replaceStoredProcedure(sprocLink: string, sproc: SprocResource, options: RequestOptions, callback: Callback); - replaceTrigger; - replaceUser(userLink: string, user: UserResource, options: RequestOptions, callback: Callback); - replaceUserDefinedFunction; - updateMedia; - upsertAttachment; - upsertAttachmentAndUploadMedia; - upsertDocument(collectionLink: string, body: DocumentResource, options: RequestOptions, callback: Callback); - upsertPermission(userLink: string, body: PermissionResource, options: RequestOptions, callback: Callback); - upsertStoredProcedure(collectionLink: string, sproc: SprocResource, options: RequestOptions, callback: Callback); - upsertTrigger; - upsertUser(dbLink: string, body: UserResource, options: RequestOptions, callback: Callback); - upsertUserDefinedFunction; - } +// import DocumentDB module +const documentdb = require("documentdb"); - /** Constructor for the native DocumentClient instance */ - export interface DocumentClient_Ctor { - new (urlConnection: string, auth: AuthenticationOptions, connectionPolicy?: ConnectionPolicy, consistencyLevel?: ConsistencyLevel): DocumentClient; - } +/** Constructor for the DocumentClient instance */ +export const DocumentClient: { + new (urlConnection: string, auth: AuthenticationOptions, connectionPolicy?: ConnectionPolicy, consistencyLevel?: ConsistencyLevel): DocumentClient; +} = documentdb.DocumentClient; - /** Callback as used by the native DocumentClient */ - export type Callback = (error: ClientError, resource: {}, responseHeaders: {}) => void; +/** A DocumentClient instance */ +export interface DocumentClient { + createCollection(dbLink: string, body: Partial & Identifiable, options: RequestOptions | undefined, callback: Callback): void; + createDatabase(body: Partial & Identifiable, options: RequestOptions | undefined, callback: Callback): void; + createDocument(collectionLink: string, body: Partial, options: RequestOptions | undefined, callback: Callback): void; + createPermission(userLink: string, body: Partial, options: RequestOptions | undefined, callback: Callback): void; + createStoredProcedure(collectionLink: string, sproc: WriteSprocResource & Identifiable, options: RequestOptions | undefined, callback: Callback): void; + createUser(dbLink: string, body: Partial & Identifiable, options: RequestOptions | undefined, callback: Callback): void; + deleteCollection(collectionLink: string, options: RequestOptions | undefined, callback: Callback): void; + deleteDatabase(dbLink: string, options: RequestOptions | undefined, callback: Callback): void; + deleteDocument(documentLink: string, options: RequestOptions | undefined, callback: Callback): void; + deletePermission(permissionLink: string, options: RequestOptions | undefined, callback: Callback): void; + deleteStoredProcedure(sprocLink: string, options: RequestOptions | undefined, callback: Callback): void; + deleteUser(userLink: string, options: RequestOptions | undefined, callback: Callback): void; + executeStoredProcedure(sprocLink: string, params: any[], options: RequestOptions | undefined, callback: Callback): void; + getDatabaseAccount(callback: (error: ClientError, databaseAccount: DatabaseAccount) => void): void; + getReadEndpoint(callback: (url: string) => void): void; + getWriteEndpoint(callback: (url: string) => void): void; + queryCollections(dbLink: string, query: SqlQuery, options?: FeedOptions): QueryIterator; + queryConflicts(collectionLink: string, query: SqlQuery, options?: FeedOptions): QueryIterator; + queryDatabases(query: SqlQuery, options?: FeedOptions): QueryIterator; + queryDocuments(collectionLink: string, query: SqlQuery, options?: FeedOptions): QueryIterator; + queryOffers(query: SqlQuery, options?: FeedOptions): QueryIterator; + queryPermissions(userLink: string, query: SqlQuery, options?: FeedOptions): QueryIterator; + queryStoredProcedures(collectionLink: string, query: SqlQuery, options?: FeedOptions): QueryIterator; + queryUsers(dbLink: string, query: SqlQuery, options?: FeedOptions): QueryIterator; + readCollection(collectionLink: string, options: RequestOptions | undefined, callback: Callback): void; + readCollections(dbLink: string, options?: FeedOptions): QueryIterator; + readConflict(conflictLink: string, options: RequestOptions | undefined, callback: Callback): void; + readConflicts(collectionLink: string, options?: FeedOptions): QueryIterator; + readDatabase(dbLink: string, options: RequestOptions | undefined, callback: Callback): void; + readDatabases(options?: FeedOptions): QueryIterator; + readDocument(documentLink: string, options: RequestOptions | undefined, callback: Callback): void; + readDocuments(collectionLink: string, options?: FeedOptions): QueryIterator; + readOffer(offerLink: string, callback: Callback): void; + readOffers(options?: FeedOptions): QueryIterator; + readPermission(permissionLink: string, options: RequestOptions | undefined, callback: Callback): void; + readPermissions(userLink: string, options?: FeedOptions): QueryIterator; + readStoredProcedure(sprocLink: string, options: RequestOptions | undefined, callback: Callback): void; + readStoredProcedures(collectionLink: string, options?: FeedOptions): QueryIterator; + readUser(userLink: string, options: RequestOptions | undefined, callback: Callback): void; + readUsers(dbLink: string, options?: FeedOptions): QueryIterator; + replaceDocument(documentLink: string, document: Partial & Identifiable, options: RequestOptions | undefined, callback: Callback): void; + replaceOffer(offerLink: string, offer: OfferResource, callback: Callback): void; + replacePermission(permissionLink: string, permission: PermissionResource, options: RequestOptions | undefined, callback: Callback): void; + replaceStoredProcedure(sprocLink: string, sproc: SprocResource, options: RequestOptions | undefined, callback: Callback): void; + replaceUser(userLink: string, user: UserResource, options: RequestOptions | undefined, callback: Callback): void; + upsertDocument(collectionLink: string, body: Partial, options: RequestOptions | undefined, callback: Callback): void; + upsertPermission(userLink: string, body: Partial & Identifiable, options: RequestOptions | undefined, callback: Callback): void; + upsertStoredProcedure(collectionLink: string, sproc: Partial & Identifiable, options: RequestOptions | undefined, callback: Callback): void; + upsertUser(dbLink: string, body: Partial & Identifiable, options: RequestOptions | undefined, callback: Callback): void; - /** Error returned by documentdb methods */ - export interface ClientError { - code: number; - body: string; - } + // TODO: add typings for these methods: + createAttachment: any; + createAttachmentAndUploadMedia: any; + createTrigger: any; + createUserDefinedFunction: any; + deleteAttachment: any; + deleteConflict: any; + deleteTrigger: any; + deleteUserDefinedFunction: any; + queryAttachments: any; + queryTriggers: any; + queryUserDefinedFunctions: any; + readAttachment: any; + readAttachments: any; + readMedia: any; + readTrigger: any; + readTriggers: any; + readUserDefinedFunction: any; + readUserDefinedFunctions: any; + replaceAttachment: any; + replaceCollection: any; + replaceTrigger: any; + replaceUserDefinedFunction: any; + updateMedia: any; + upsertAttachment: any; + upsertAttachmentAndUploadMedia: any; + upsertTrigger: any; + upsertUserDefinedFunction: any; +} - /** Resoure object */ - export interface Resource extends Object { - id: string; - _rid?: string; - _self?: string; - _etag?: string; - _ts?: number; - _attachments?: string; - } +/** Callback as used by DocumentClient */ +export type Callback = + (error: ClientError, resource: T, responseHeaders: {}) => void; - /** User resource (no special properties) */ - export interface UserResource extends Resource { } +/** Object with an ID string */ +export interface Identifiable { id: string }; - /** Document resource */ - export interface DocumentResource extends Resource { - ttl?: number; - } +/** Error returned by documentdb methods */ +export interface ClientError { + /** HTTP status code (e.g. 400 for malformed requests, 401 and 403 for auth errors, 404 for resource not found, 408 for when an internal operation timed out, 409 for resource conflicts, 412 for etag mismatches, 413 for entity too large, 429 for request rate too large, 449 for generic write errors that can be retried, and 500+ for server errors) */ + code: number; + /** Error body, usually another object encoded as JSON */ + body: string; +} - /** Document resource with all properties */ - export interface ReadDocumentResource extends DocumentResource { - id: string; - _rid: string; - _self: string; - _etag: string; - _ts: number; - _attachments?: string; - ttl?: number; - } +/** Resoure object */ +export interface Resource extends Object { + /** Unique resource ID, less than 255 characters */ + id: string; + /** System generated resource ID for this resource */ + _rid: string; + /** System generated unique addressable URI for this resource */ + _self: string; + /** System generated entity tag for this resource in its current version, for optimistic concurrency control */ + _etag: string; + /** System generated last updated timestamp for this resource (in seconds since UNIX epoch) */ + _ts: number; + /** System generated addressable path for the attachments resource */ + _attachments: string; +} - /** Collection resource */ - export interface CollectionResource extends Resource { - indexingPolicy?: IndexingPolicy; - defaultTtl?: number; - }; +/** User resource (no special properties) */ +export interface UserResource extends Resource { + /** System generated addressable path for the feed of permissions resource */ + _permissions: string; +} - /** Permission resource */ - export interface PermissionResource extends Resource { - permissionMode: "none" | "read" | "all"; - resource: string; - }; +/** Document resource */ +export interface DocumentResource extends Resource { + ttl?: number; +} - /** Stored procedure resource */ - export interface SprocResource extends Resource { - serverScript: Function | string; - } +/** Database resource */ +export interface DatabaseResource extends Resource { + /** System generated addressable path for the collections resource */ + _colls: string; + /** System generated addressable path for the users resource */ + _users: string; +}; - /** Offer (throughput provisioning) information resource */ - export interface OfferResource extends Resource { - offerVersion: string; - offerType: string; - content: { - offerThroughput: number; - }; - offerResourceId: string; - resource: string; - } +/** Collection resource */ +export interface CollectionResource extends Resource { + /** Indexing policy for this collection, if any */ + indexingPolicy?: IndexingPolicy; + /** Partition key for this collection, if any */ + partitionKey?: PartitionKeyDefinition; + /** Default TTL value for document resources, if any (in seconds, OR -1 for no expiry by default) */ + defaultTtl?: number; + /** System generated addressable path for the documents resource */ + _docs: string; + /** System generated addressable path for the stored procedures resource */ + _sprocs: string; + /** System generated addressable path for the triggers resource */ + _triggers: string; + /** System generated addressable path for the UDFs resource */ + _udfs: string; + /** System generated addressable path for the conflicts resource */ + _conflicts: string; +}; - /** Consistency level constants */ - export type ConsistencyLevel = "Strong" | "BoundedStaleness" | "Session" | "Eventual"; +/** Permission resource */ +export interface PermissionResource extends Resource { + /** Access mode on the resource for the user: All or Read */ + permissionMode: "Read" | "All"; + /** Full addressable path of the resource associated with the permission */ + resource: string; +}; - /** Query type: either a plain string or a structure with parameters */ - export type SqlQuery = string | { - query: string; - parameters: { name: string; value: any }[]; +/** Stored procedure resource */ +export interface SprocResource extends Resource { + /** The unique name of this stored procedure */ + id: string; + /** The stored procedure function as a string */ + body: string; +} + +/** Stored procedure resource, with script as a JavaScript function */ +export type WriteSprocResource = Partial & { + /** The unique name of this stored procedure */ + id: string; + /** The stored procedure function as a JavaScript Function instance */ + serverScript: Function; +} | Partial & { + /** The unique name of this stored procedure */ + id: string; + /** The stored procedure function as a string */ + body: string; +}; + +/** Offer (throughput provisioning) information resource */ +export interface OfferResource extends Resource { + offerVersion: "V2"; + offerType: "Invalid"; + content: { + /** Throughput for the associated collection, must be in a multiple of 100 */ + offerThroughput: number; }; + offerResourceId: string; + resource: string; +} - /** Authentication options used by the DocumentClient constructor */ - export interface AuthenticationOptions { - masterKey: string; - resourceTokens?: { - [resourceId: string]: string - }; - permissionFeed?: any[]; - } +/** Consistency level constants */ +export type ConsistencyLevel = "Strong" | "BoundedStaleness" | "Session" | "Eventual"; - /** Native connection policy interface */ - export interface ConnectionPolicy { - MediaReadMode?: "Buffered" | "Streamed"; - MediaRequestTimeout?: number; - RequestTimeout?: number; - EnableEndpointDiscovery?: boolean; - PreferredLocations?: string[]; - RetryOptions: { - MaxRetryAttemptCount?: number; - FixedRetryIntervalInMilliseconds?: number; - MaxWaitTimeInSeconds?: number; - } +/** Query type: either a plain string or a structure with parameters */ +export type SqlQuery = string | { + /** The SQL query expressed as a string */ + query: string; + /** SQL query parameters as a list of name-value pairs */ + parameters: Array<{ name: string; value: any }>; +}; + +/** Authentication options used by the DocumentClient constructor */ +export interface AuthenticationOptions { + masterKey: string; + resourceTokens?: { + [resourceId: string]: string + }; + permissionFeed?: any[]; +} + +/** DocumentClient connection policy interface */ +export interface ConnectionPolicy { + MediaReadMode?: "Buffered" | "Streamed"; + MediaRequestTimeout?: number; + RequestTimeout?: number; + EnableEndpointDiscovery?: boolean; + PreferredLocations?: string[]; + RetryOptions: { + MaxRetryAttemptCount?: number; + FixedRetryIntervalInMilliseconds?: number; + MaxWaitTimeInSeconds?: number; } +} - /** Native indexing policy interface */ - export interface IndexingPolicy { - automatic?: boolean; - indexingMode?: "Consistent" | "Lazy"; - includedPaths: { - Path: string; - Indexes: { - Kind: "Hash" | "Range" | "Spatial"; - DataType: string; - Precision: number; - }[]; +/** DocumentClient indexing policy interface */ +export interface IndexingPolicy { + automatic?: boolean; + indexingMode?: "Consistent" | "Lazy"; + includedPaths: { + Path: string; + Indexes: { + Kind: "Hash" | "Range" | "Spatial"; + DataType: string; + Precision: number; }[]; - excludedPaths: { - Path: string; - }[]; - } + }[]; + excludedPaths: { + Path: string; + }[]; +} - /** Native request options interface */ - export interface RequestOptions { - preTriggerInclude?: string; - postTriggerInclude?: string; - accessCondition?: { - type: "IfMatch" | "IfNoneMatch", condition: string; - }; - indexingDirective?: string; - consistencyLevel?: ConsistencyLevel; - sessionToken?: string; - resourceTokenExpirySeconds?: number; - offerType?: string; - offerThroughput?: number; - partitionKey?: {}; - disableAutomaticIdGeneration?: boolean; - } +/** DocumentClient partition key definition interface */ +export interface PartitionKeyDefinition { + paths: string[]; + kind: "Hash" +} - /** Account information */ - export interface DatabaseAccount { - DatabasesLink: string; - MediaLink: string; - MaxMediaStorageUsageInMB: string | number; - CurrentMediaStorageUsageInMB: string | number; - ConsumedDocumentStorageInMB: number; - ReservedDocumentStorageInMB: number; - ProvisionedDocumentStorageInMB: number; - ConsistencyPolicy: { - defaultConsistencyLevel: ConsistencyLevel; - maxStalenessPrefix: number; - maxStalenessIntervalInSeconds: number; - }; - WritableLocations: string[]; - ReadableLocations: string[]; - } +/** DocumentClient request options interface */ +export interface RequestOptions { + preTriggerInclude?: string; + postTriggerInclude?: string; + accessCondition?: { + type: "IfMatch" | "IfNoneMatch", condition: string; + }; + indexingDirective?: string; + consistencyLevel?: ConsistencyLevel; + sessionToken?: string; + resourceTokenExpirySeconds?: number; + offerType?: string; + offerThroughput?: number; + partitionKey?: {}; + disableAutomaticIdGeneration?: boolean; + enableScriptLogging?: boolean; +} - /** Native feed options interface */ - export interface FeedOptions { - maxItemCount?: number; - continuation?: string; - sessionToken?: string; - partitionKey?: {}; - } +/** Account information */ +export interface DatabaseAccount { + DatabasesLink: string; + MediaLink: string; + MaxMediaStorageUsageInMB: string | number; + CurrentMediaStorageUsageInMB: string | number; + ConsumedDocumentStorageInMB: number; + ReservedDocumentStorageInMB: number; + ProvisionedDocumentStorageInMB: number; + ConsistencyPolicy: { + defaultConsistencyLevel: ConsistencyLevel; + maxStalenessPrefix: number; + maxStalenessIntervalInSeconds: number; + }; + WritableLocations: string[]; + ReadableLocations: string[]; +} - /** Native query iterator for iterating over a (future) response */ - export interface QueryIterator { - current(callback: (error: ClientError, element: ResourceT) => void); - executeNext(callback: (error: ClientError, list: ResourceT[]) => void); - forEach(callback: (error: ClientError, element: ResourceT | undefined) => void); - nextItem(callback: (error: ClientError, element: ResourceT) => void); - reset(); - toArray(callback: (error: ClientError, list: ResourceT[]) => void); - } -} \ No newline at end of file +/** DocumentClient feed options interface */ +export interface FeedOptions { + maxItemCount?: number; + continuation?: string; + sessionToken?: string; + partitionKey?: {}; + enableScanInQuery?: boolean; + enableCrossPartitionQuery?: boolean; +} + +/** DocumentClient query iterator for iterating over a (future) result set */ +export interface QueryIterator { + current(callback: (error: ClientError, element: T) => void): void; + executeNext(callback: (error: ClientError, list: T[]) => void): void; + forEach(callback: (error: ClientError, element: T | undefined) => void): void; + nextItem(callback: (error: ClientError, element: T) => void): void; + reset(): void; + toArray(callback: (error: ClientError, list: T[]) => void): void; +} diff --git a/src/tsconfig.json b/src/tsconfig.json index f84fc12..a62408f 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -4,6 +4,8 @@ "declarationDir": "../typings", "target": "es6", "module": "commonjs", + "strictNullChecks": true, + "noImplicitAny": true, "noImplicitReturns": true, "noImplicitThis": true, "noUnusedLocals": true,