documentdb-typescript
Version:
TypeScript API for Microsoft Azure DocumentDB
354 lines (353 loc) • 17.7 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const Database_1 = require("./Database");
const DocumentStream_1 = require("./DocumentStream");
const Util_1 = require("./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 */
var StoreMode;
(function (StoreMode) {
StoreMode[StoreMode["Upsert"] = 0] = "Upsert";
StoreMode[StoreMode["CreateOnly"] = 1] = "CreateOnly";
StoreMode[StoreMode["UpdateOnly"] = 2] = "UpdateOnly";
StoreMode[StoreMode["UpdateOnlyIfNoChange"] = 3] = "UpdateOnlyIfNoChange";
})(StoreMode = exports.StoreMode || (exports.StoreMode = {}));
;
/** Represents a DocumentDB collection */
class Collection {
constructor(id, ...info) {
if (!id)
throw new Error("Collections must have a name");
this.id = id;
if (info[0] instanceof Database_1.Database) {
// use given database and possibly self link passed by database
this.database = info[0];
this._self = info[1];
}
else {
// construct database with given data
this.database = new Database_1.Database(info[0], info[1], info[2]);
}
}
/** The partial resource URI for this database, i.e. `"/dbs/.../colls/..."` */
get path() {
return this._self ||
"dbs/" + this.database.id + "/colls/" + encodeURIComponent(this.id);
}
/** Open and validate the connection, check that this collection exists */
openAsync(maxRetries, options) {
return __awaiter(this, void 0, void 0, function* () {
if (this._self)
return this;
yield this.database.openAsync(maxRetries);
let tryGetCollection = (callback) => this.database.client.log("Reading collection " + this.path) &&
this.database.client.documentClient.readCollection(this.path, options, callback);
var resource = yield Util_1.curryPromise(tryGetCollection, this.database.client.timeout, maxRetries)();
this._self = resource["_self"];
return this;
});
}
/** Open and validate the connection, find or create collection resource (does not create the database) */
openOrCreateAsync(createThroughput, indexingPolicy, defaultTtl, maxRetries, options) {
return __awaiter(this, void 0, void 0, function* () {
if (this._self)
return this;
yield this.database.openAsync(maxRetries);
var resource;
try {
let tryGetCollection = (callback) => this.database.client.log("Reading collection " + this.id) &&
this.database.client.documentClient.readCollection(this.path, options, callback);
resource = yield Util_1.curryPromise(tryGetCollection, this.database.client.timeout, maxRetries)();
}
catch (err) {
if (err.code == 404 /* not found */) {
// create the collection now
var data = { id: this.id };
if (indexingPolicy)
data.indexingPolicy = indexingPolicy;
if (defaultTtl !== undefined)
data.defaultTtl = defaultTtl;
try {
let tryCreateCollection = (callback) => this.database.client.log("Creating collection " + this.id) &&
this.database.client.documentClient.createCollection(this.database.path, data, { offerThroughput: createThroughput }, callback);
resource = yield Util_1.curryPromise(tryCreateCollection, this.database.client.timeout)();
}
catch (err) {
if (err.code == 409 /* conflict */) {
this.database.client.log("Collection conflict, retrying...");
yield Util_1.sleepAsync(1000);
return yield this.openAsync();
}
throw err;
}
}
else
throw err;
}
this._self = resource["_self"];
return this;
});
}
/** Open and validate the connection, find or create collection resource (also creates the database if needed) */
openOrCreateDatabaseAsync(createThroughput, indexingPolicy, defaultTtl, maxRetries) {
return __awaiter(this, void 0, void 0, function* () {
if (this._self)
return this;
yield this.database.openOrCreateAsync(maxRetries);
yield this.openOrCreateAsync(createThroughput, indexingPolicy, defaultTtl, maxRetries);
return this;
});
}
/** Get offer (throughput provisioning) information */
getOfferInfoAsync(maxRetries, options) {
return __awaiter(this, void 0, void 0, function* () {
yield this.openAsync();
let tryGetOffer = (callback) => 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 }]
}, options).toArray(callback);
var offers = yield Util_1.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]));
});
}
/** Set provisioned throughput */
setOfferInfoAsync(throughput) {
return __awaiter(this, void 0, void 0, function* () {
yield this.openAsync();
if (!this._offer)
yield this.getOfferInfoAsync();
var offer = this._offer;
if (!offer.content || !offer.content.offerThroughput)
throw new Error("Unknown offer type");
offer.content.offerThroughput = throughput;
let trySetOffer = (callback) => this.database.client.log("Setting offer info for " + this.id) &&
this.database.client.documentClient.replaceOffer(offer._self, offer, callback);
this._offer = yield Util_1.curryPromise(trySetOffer, this.database.client.timeout)();
});
}
/** Delete this collection */
deleteAsync(maxRetries, options) {
return __awaiter(this, void 0, void 0, function* () {
yield this.openAsync();
let tryDelete = (callback) => this.database.client.log("Deleting collection: " + this.id) &&
this.database.client.documentClient.deleteCollection(this._self, options, callback);
yield Util_1.curryPromise(tryDelete, this.database.client.timeout, maxRetries, 500, true)();
delete this._self;
});
}
/** 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` */
storeDocumentAsync(data, mode, maxRetries, options) {
return __awaiter(this, void 0, void 0, function* () {
yield this.openAsync();
if (!(data instanceof Object))
throw new TypeError();
var tryStore;
switch (mode) {
case StoreMode.UpdateOnlyIfNoChange:
if (!data._etag)
throw new Error("Document _etag missing");
options = Object.assign({
accessCondition: {
type: "IfMatch",
condition: data._etag
}
}, options);
// continue with update...
case StoreMode.UpdateOnly:
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(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, 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, options, callback);
}
return yield Util_1.curryPromise(tryStore, this.database.client.timeout, maxRetries)();
});
}
findDocumentAsync(obj, maxRetries, options) {
return __awaiter(this, void 0, void 0, function* () {
yield this.openAsync();
// read using readDocument if possible
if (typeof obj === "string" ||
obj && (typeof obj._self === "string" ||
(typeof obj.id === "string"))) {
var docURI;
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 tryReadDoc = (callback) => this.database.client.log("Reading document " + docURI) &&
this.database.client.documentClient.readDocument(docURI, options, callback);
let result = yield Util_1.curryPromise(tryReadDoc, this.database.client.timeout, maxRetries, undefined, true)();
if (typeof obj !== "string" && !obj._self) {
// check that other properties match, too
for (var prop in obj) {
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
if (obj[prop] !== result[prop])
throw new Error("Resource not found");
}
}
}
return result;
}
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 = {
query: "select top 1 * from c where",
parameters: []
};
if (obj instanceof Object) {
var i = 0;
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[${JSON.stringify(prop)}] = ${++i}`;
q.parameters.push({
name: "@_value_" + i,
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) => this.database.client.log("Querying collection " + this.id + ": " +
JSON.stringify(q)) &&
this.database.client.documentClient.queryDocuments(this._self, q, options).toArray(callback);
var results = yield Util_1.curryPromise(tryQuery, this.database.client.timeout, maxRetries)();
if (!results || !results.length)
throw new Error("Resource not found");
return results[0];
});
}
existsAsync(obj, maxRetries, options) {
return __awaiter(this, void 0, void 0, function* () {
yield this.openAsync();
// use queryDocuments with given ID or properties
var q = {
query: "select value count(1) from c where",
parameters: []
};
if (typeof obj === "string") {
q.query += " c.id = @id";
q.parameters.push({ name: "@id", value: obj });
}
else if (obj instanceof Object) {
var i = 0;
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[${JSON.stringify(prop)}] = ${++i}`;
q.parameters.push({
name: "@_value_" + i,
value: obj[prop]
});
}
}
}
if (!q.parameters.length)
q.query += " true";
// run the query and return true only if count >= 1
let tryQuery = (callback) => this.database.client.log("Querying collection " + this.id + ": " +
JSON.stringify(q)) &&
this.database.client.documentClient
.queryDocuments(this._self, q, options)
.toArray(callback);
var results = yield Util_1.curryPromise(tryQuery, this.database.client.timeout, maxRetries)();
return !!results && results[0] >= 1;
});
}
queryDocuments(query, options) {
if (typeof options === "number")
options = { maxItemCount: options };
var uid = ++_queryUID;
if (query === undefined) {
// use readDocuments to get all documents
return DocumentStream_1.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_1.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)));
}
}
deleteDocumentAsync(v, maxRetries, options) {
return __awaiter(this, void 0, void 0, function* () {
yield this.openAsync();
var id = typeof v === "string" ? v : v.id;
var docURI;
try {
docURI = this._getDocURI(v);
}
catch (all) { }
if (!docURI) {
// ID may contain invalid characters, find _self instead
var obj = yield this.queryDocuments({
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) => this.database.client.log("Deleting: " + (id || "<no ID>")) &&
this.database.client.documentClient.deleteDocument(docURI, options, callback);
yield Util_1.curryPromise(tryDelete, this.database.client.timeout, maxRetries, 500, true)();
});
}
/** @internal Helper function that returns a document URI for given ID or object */
_getDocURI(v) {
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;
}
}
exports.Collection = Collection;