UNPKG

documentdb-typescript

Version:
354 lines (353 loc) 17.7 kB
"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)}] = @_value_${++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)}] = @_value_${++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;