Skip to content

Commit

Permalink
1.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
jcormont committed Mar 16, 2017
1 parent 8bb8e20 commit a5fe2c9
Show file tree
Hide file tree
Showing 9 changed files with 772 additions and 464 deletions.
95 changes: 62 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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");
}
```

Expand All @@ -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 {
Expand All @@ -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");
Expand Down Expand Up @@ -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<FooResult>("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(
Expand Down
13 changes: 11 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -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"
}
}
68 changes: 35 additions & 33 deletions src/Client.ts
Original file line number Diff line number Diff line change
@@ -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<string, Client>();

Expand All @@ -16,7 +10,7 @@ var _uid = 1;

/** Represents a DocumentDB endpoint */
export class Client {
constructor(url?: string, masterKey?: string) {
constructor(url?: string, masterKey = "<no_key>") {
this.url = url || "";
this.authenticationOptions = { masterKey };
}
Expand All @@ -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<any> {
Expand All @@ -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) }));
});
}

Expand All @@ -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;
Expand All @@ -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)
Expand All @@ -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) => {
Expand All @@ -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<any>;
private _open?: PromiseLike<any>;

/** @internal */
private _client: _DocumentDB.DocumentClient | null = null;
private _client?: _DocumentDB.DocumentClient;

/** @internal */
private _closed: boolean;
private _closed?: boolean;

/** @internal */
private _uid = _uid++;
}
}
Loading

0 comments on commit a5fe2c9

Please sign in to comment.