From 4ed63e242a5ec3f714e9eb0cad5fad5573e21ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Nguyen?= Date: Wed, 23 Sep 2015 09:51:38 +0200 Subject: [PATCH] Added definitions and tests for DocumentDB Server Side JavaScript SDK Test samples taken from: - http://dl.windowsazure.com/documentDB/jsserverdocs/Collection.html - https://github.com/Azure/azure-documentdb-js-server/tree/master/samples --- documentdb-server/documentdb-server-tests.ts | 1016 ++++++++++++++++++ documentdb-server/documentdb-server.d.ts | 541 ++++++++++ 2 files changed, 1557 insertions(+) create mode 100644 documentdb-server/documentdb-server-tests.ts create mode 100644 documentdb-server/documentdb-server.d.ts diff --git a/documentdb-server/documentdb-server-tests.ts b/documentdb-server/documentdb-server-tests.ts new file mode 100644 index 00000000000000..722fdbec5c0544 --- /dev/null +++ b/documentdb-server/documentdb-server-tests.ts @@ -0,0 +1,1016 @@ +/// + +// Samples taken from http://dl.windowsazure.com/documentDB/jsserverdocs/Collection.html +function chain() { + var name: string = "John"; + var result: IQueryResponse = __.chain() + .filter(function (doc: any) { return doc.name == name; }) + .map(function (doc: any) { return { name: doc.name, age: doc.age }; }) + .value(); + if (!result.isAccepted) throw new Error("The call was not accepted"); +} +function filter() { + // Example 1: get documents(people) with age < 30. + var result: IQueryResponse = __.filter(function (doc: any) { return doc.age < 30; }); + if (!result.isAccepted) throw new Error("The call was not accepted"); + // Example 2: get documents (people) with age < 30 and select only name. + var result: IQueryResponse = __.chain() + .filter(function (doc: any) { return doc.age < 30; }) + .pluck("name") + .value(); + if (!result.isAccepted) throw new Error("The call was not accepted"); + // Example 3: get document (person) with id = 1 and delete it. + var result: IQueryResponse = __.filter(function (doc: any) { return doc.id === 1; }, function (err: IFeedCallbackError, feed: Array, options: IFeedCallbackOptions) { + if (err) throw err; + if (!__.deleteDocument(feed[0].getSelfLink())) throw new Error("deleteDocument was not accepted"); + }); + if (!result.isAccepted) throw new Error("The call was not accepted"); +} +function flatten() { + // Get documents (people) with age < 30, select tags (an array property) + // and flatten the result into one array for all documents. + var result: IQueryResponse = __.chain() + .filter(function (doc: any) { return doc.age < 30; }) + .map(function (doc: any) { return doc.tags; }) + .flatten() + .value(); + if (!result.isAccepted) throw new Error("The call was not accepted"); +} +function map() { + // Example 1: select only name and age for each document (person). + var result: IQueryResponse = __.map(function (doc: any) { return { name: doc.name, age: doc.age }; }); + if (!result.isAccepted) throw new Error("The call was not accepted"); + // Example 2: select name and age for each document (person), and return only people with age < 30. + var result: IQueryResponse = __.chain() + .map(function (doc: any) { return { name: doc.name, age: doc.age }; }) + .filter(function (doc: any) { return doc.age < 30; }) + .value(); + if (!result.isAccepted) throw new Error("The call was not accepted"); +} +function pluck() { + // Get documents (people) with age < 30 and select only name. + var result: IQueryResponse = __.chain() + .filter(function (doc: any) { return doc.age < 30; }) + .pluck("name") + .value(); + if (!result.isAccepted) throw new Error("The call was not accepted"); +} +function sortBy() { + // Example 1: sort documents (people) by age + var result: IQueryResponse = __.sortBy(function (doc: any) { return doc.age; }) + if (!result.isAccepted) throw new Error("The call was not accepted"); + // Example 2: sortBy in a chain by name + var result: IQueryResponse = __.chain() + .filter(function (doc: any) { return doc.age < 30; }) + .sortBy(function (doc: any) { return doc.name; }) + .value(); + if (!result.isAccepted) throw new Error("The call was not accepted"); +} +function sortByDescending() { + // Example 1: sort documents (people) by age in descending order + var result: IQueryResponse = __.sortByDescending(function (doc: any) { return doc.age; }) + if (!result.isAccepted) throw new Error("The call was not accepted"); + // Example 2: sortBy in a chain by name in descending order + var result: IQueryResponse = __.chain() + .filter(function (doc: any) { return doc.age < 30; }) + .sortByDescending(function (doc: any) { return doc.name; }) + .value(); + if (!result.isAccepted) throw new Error("The call was not accepted"); +} +function value() { + // Example 1: use defaults: the result goes to the response body. + var result: IQueryResponse = __.chain() + .filter(function (doc: any) { return doc.name == "John"; }) + .pluck("age") + .value(); + if (!result.isAccepted) throw new Error("The call was not accepted"); + // Example 2: use options and callback. + function usingOptionsAndCallback (continuationToken: string) { + var result = __.chain() + .filter(function (doc: any) { return doc.name == "John"; }) + .pluck("age") + .value({ continuation: continuationToken }, function (err: IFeedCallbackError, feed: Array, options: IFeedCallbackOptions) { + if (err) throw err; + __.response.setBody({ + result: feed, + continuation: options.continuation + }); + }); + if (!result.isAccepted) throw new Error("The call was not accepted"); + } +} + +// Samples taken from https://github.com/Azure/azure-documentdb-js-server/tree/master/samples +/** +* This script called as stored procedure to import lots of documents in one batch. +* The script sets response body to the number of docs imported and is called multiple times +* by the client until total number of docs desired by the client is imported. +* @param {Object[]} docs - Array of documents to import. +*/ +function bulkImport(docs: Array) { + var collection: ICollection = getContext().getCollection(); + var collectionLink: string = collection.getSelfLink(); + + // The count of imported docs, also used as current doc index. + var count: number = 0; + + // Validate input. + if (!docs) throw new Error("The array is undefined or null."); + + var docsLength: number = docs.length; + if (docsLength == 0) { + getContext().getResponse().setBody(0); + return; + } + + // Call the CRUD API to create a document. + tryCreate(docs[count], callback); + + // Note that there are 2 exit conditions: + // 1) The createDocument request was not accepted. + // In this case the callback will not be called, we just call setBody and we are done. + // 2) The callback was called docs.length times. + // In this case all documents were created and we don't need to call tryCreate anymore. Just call setBody and we are done. + function tryCreate(doc: Object, callback: (err: IRequestCallbackError, doc: Object, options: IRequestCallbackOptions) => void): void { + var isAccepted = collection.createDocument(collectionLink, doc, callback); + + // If the request was accepted, callback will be called. + // Otherwise report current count back to the client, + // which will call the script again with remaining set of docs. + // This condition will happen when this stored procedure has been running too long + // and is about to get cancelled by the server. This will allow the calling client + // to resume this batch from the point we got to before isAccepted was set to false + if (!isAccepted) getContext().getResponse().setBody(count); + } + + // This is called when collection.createDocument is done and the document has been persisted. + function callback(err: IRequestCallbackError, doc: Object, options: IRequestCallbackOptions) { + if (err) throw err; + + // One more document has been inserted, increment the count. + count++; + + if (count >= docsLength) { + // If we have created all documents, we are done. Just set the response. + getContext().getResponse().setBody(count); + } else { + // Create next document. + tryCreate(docs[count], callback); + } + } +} + +/** +* This is executed as stored procedure to count the number of docs in the collection. +* To avoid script timeout on the server when there are lots of documents (100K+), the script executed in batches, +* each batch counts docs to some number and returns continuation token. +* The script is run multiple times, starting from empty continuation, +* then using continuation returned by last invocation script until continuation returned by the script is null/empty string. +* +* @param {String} filterQuery - Optional filter for query (e.g. "SELECT * FROM docs WHERE docs.category = 'food'"). +* @param {String} continuationToken - The continuation token passed by request, continue counting from this token. +*/ +function count(filterQuery: string, continuationToken: string) { + var collection: ICollection = getContext().getCollection(); + var maxResult: number = 25; // MAX number of docs to process in one batch, when reached, return to client/request continuation. + // intentionally set low to demonstrate the concept. This can be much higher. Try experimenting. + // We've had it in to the high thousands before seeing the stored proceudre timing out. + + // The number of documents counted. + var result: number = 0; + + tryQuery(continuationToken); + + // Helper method to check for max result and call query. + function tryQuery(nextContinuationToken: string) { + var responseOptions: Object = { continuation: nextContinuationToken, pageSize: maxResult }; + + // In case the server is running this script for long time/near timeout, it would return false, + // in this case we set the response to current continuation token, + // and the client will run this script again starting from this continuation. + // When the client calls this script 1st time, is passes empty continuation token. + if (result >= maxResult || !query(responseOptions)) { + setBody(nextContinuationToken); + } + } + + function query(responseOptions: IFeedOptions) { + // For empty query string, use readDocuments rather than queryDocuments -- it's faster as doesn't need to process the query. + return (filterQuery && filterQuery.length) ? + collection.queryDocuments(collection.getSelfLink(), filterQuery, responseOptions, onReadDocuments) : + collection.readDocuments(collection.getSelfLink(), responseOptions, onReadDocuments); + } + + // This is callback is called from collection.queryDocuments/readDocuments. + function onReadDocuments(err: IFeedCallbackError, docFeed: Array, responseOptions: IFeedCallbackOptions) { + if (err) { + throw 'Error while reading document: ' + err; + } + + // Increament the number of documents counted so far. + result += docFeed.length; + + // If there is continuation, call query again with it, + // otherwise we are done, in which case set continuation to null. + if (responseOptions.continuation) { + tryQuery(responseOptions.continuation); + } else { + setBody(null); + } + } + + // Set response body: use an object the client is expecting (2 properties: result and continuationToken). + function setBody(continuationToken: string) { + var body: Object = { count: result, continuationToken: continuationToken }; + getContext().getResponse().setBody(body); + } +} + +/** +* This is run as stored procedure and does the following: +* - create ordered result set (result) which is an array sorted by orderByFieldName parameter. +* - call collection.queryDocuments. +* - in the callback for each document, insert into an array (result) +* - in the end, sort the resulting array and return it to the client +* +* Important notes: +* - The resulting record set could be too large to fit into one response +* - To walk around that, we setBody by one element and catch the REQUEST_ENTITY_TOO_LARGE exception. +* When we get the exception, return resulting set to the client with continuation token +* to continue from item index specified by this token. +* - Note that when continuation is called, it will be different transaction +* +* @param {String} filterQuery - Optional filter for query. +* @param {String} orderByFieldName - The name of the field to order by resulting set. +* @param {String} continuationToken - The continuation token passed by request, continue counting from this token. +*/ +function orderBy(filterQuery: string, orderByFieldName: string, continuationToken: number) { + // HTTP error codes sent to our callback funciton by DocDB server. + var ErrorCode: any = { + REQUEST_ENTITY_TOO_LARGE: 413, + } + + var collection: ICollection = getContext().getCollection(); + var collectionLink: string = collection.getSelfLink(); + var result: Array = new Array(); + + tryQuery({}); + + function tryQuery(options: IFeedOptions) { + var isAccepted: boolean = (filterQuery && filterQuery.length) ? + collection.queryDocuments(collectionLink, filterQuery, options, callback) : + collection.readDocuments(collectionLink, options, callback) + + if (!isAccepted) throw new Error("Source dataset is too large to complete the operation."); + } + + /** + * queryDocuments callback. + * @param {Error} err - Error object in case of error/exception. + * @param {Array} queryFeed - array containing results of the query. + * @param {ResponseOptions} responseOptions. + */ + function callback(err: IFeedCallbackError, queryFeed: Array, responseOptions: IFeedCallbackOptions) { + if (err) { + throw err; + } + + // Iterate over document feed and store documents into the result array. + queryFeed.forEach(function (element: any, index: number, array: Array) { + result[result.length] = element; + }); + + if (responseOptions.continuation) { + // If there is continuation, call query again providing continuation token. + tryQuery({ continuation: responseOptions.continuation }); + } else { + // We are done with querying/got all results. Sort the results and return from the script. + result.sort(compare); + + fillResponse(); + } + } + + // Compare two objects(documents) using field specified by the orderByFieldName parameter. + // Return 0 if equal, -1 if less, 1 if greater. + function compare(x: any, y: any) { + if (x[orderByFieldName] == y[orderByFieldName]) return 0; + else if (x[orderByFieldName] < y[orderByFieldName]) return -1; + return 1; + } + + // This is called in the very end on an already sorted array. + // Sort the results and set the response body. + function fillResponse() { + // Main script is called with continuationToken which is the index of 1st item to start result batch from. + // Slice the result array and discard the beginning. From now on use the 'continuationResult' var. + var continuationResult: Array = result; + if (continuationToken) continuationResult = result.slice(continuationToken); + else continuationToken = 0; + + // Get/initialize the response. + var response: IResponse = getContext().getResponse(); + response.setBody(null); + + // Take care of response body getting too large: + // Set Response iterating by one element. When we fail due to MAX response size, return to the client requesting continuation. + var i = 0; + for (; i < continuationResult.length; ++i) { + try { + // Note: setBody is very expensive vs appendBody, use appendBody with simple approximation JSON.stringify(element). + response.appendBody(JSON.stringify(continuationResult[i])); + } catch (ex) { + if (!ex.number == ErrorCode.REQUEST_ENTITY_TOO_LARGE) throw ex; + break; + } + } + + // Now next batch to return to client has i elements. + // Slice the continuationResult if needed and discard the end. + var partialResult: Array = continuationResult; + var newContinuation: string = null; + if (i < continuationResult.length) { + partialResult = continuationResult.slice(0, i); + } + + // Finally, set response body. + response.setBody({ result: result, continuation: newContinuation }); + } +} + + +/** +* This is run as stored procedure and does the following: +* - get 1st document in the collection, convert to JSON, prepend string specified by the prefix parameter +* and set response to the result of that. +* +* @param {String} prefix - The string to prepend to the 1st document in collection. +*/ +function simple(prefix: string) { + var collection: ICollection = getContext().getCollection(); + + // Query documents and take 1st item. + var isAccepted: boolean = collection.queryDocuments( + collection.getSelfLink(), + 'SELECT * FROM root r', + function (err: IFeedCallbackError, feed: Array, options: IFeedCallbackOptions) { + if (err) throw err; + + // Check the feed and if it's empty, set the body to 'no docs found', + // Otherwise just take 1st element from the feed. + if (!feed || !feed.length) getContext().getResponse().setBody("no docs found"); + else getContext().getResponse().setBody(prefix + JSON.stringify(feed[0])); + }); + + if (!isAccepted) throw new Error("The query wasn't accepted by the server. Try again/use continuation token between API and script."); +} + +/** + * A DocumentDB stored procedure that bulk deletes documents for a given query.
+ * Note: You may need to execute this sproc multiple times (depending whether the sproc is able to delete every document within the execution timeout limit). + * + * @function + * @param {string} query - A query that provides the documents to be deleted (e.g. "SELECT * FROM c WHERE c.founded_year = 2008") + * @returns {Object.} Returns an object with the two properties:
+ * deleted - contains a count of documents deleted
+ * continuation - a boolean whether you should execute the sproc again (true if there are more documents to delete; false otherwise). + */ +function bulkDeleteSproc(query: string) { + var collection: ICollection = getContext().getCollection(); + var collectionLink: string = collection.getSelfLink(); + var response: IResponse = getContext().getResponse(); + var responseBody: any = { + deleted: 0, + continuation: true + }; + + // Validate input. + if (!query) throw new Error("The query is undefined or null."); + + tryQueryAndDelete(); + + // Recursively runs the query w/ support for continuation tokens. + // Calls tryDelete(documents) as soon as the query returns documents. + function tryQueryAndDelete(continuation?: string) { + var requestOptions: IFeedOptions = { continuation: continuation }; + + var isAccepted: boolean = collection.queryDocuments(collectionLink, query, requestOptions, function (err: IFeedCallbackError, retrievedDocs: Array, responseOptions: IFeedCallbackOptions) { + if (err) throw err; + + if (retrievedDocs.length > 0) { + // Begin deleting documents as soon as documents are returned form the query results. + // tryDelete() resumes querying after deleting; no need to page through continuation tokens. + // - this is to prioritize writes over reads given timeout constraints. + tryDelete(retrievedDocs); + } else if (responseOptions.continuation) { + // Else if the query came back empty, but with a continuation token; repeat the query w/ the token. + tryQueryAndDelete(responseOptions.continuation); + } else { + // Else if there are no more documents and no continuation token - we are finished deleting documents. + responseBody.continuation = false; + response.setBody(responseBody); + } + }); + + // If we hit execution bounds - return continuation: true. + if (!isAccepted) { + response.setBody(responseBody); + } + } + + // Recursively deletes documents passed in as an array argument. + // Attempts to query for more on empty array. + function tryDelete(documents: Array) { + if (documents.length > 0) { + // Delete the first document in the array. + var isAccepted: boolean = collection.deleteDocument(documents[0]._self, {}, function (err, responseOptions) { + if (err) throw err; + + responseBody.deleted++; + documents.shift(); + // Delete the next document in the array. + tryDelete(documents); + }); + + // If we hit execution bounds - return continuation: true. + if (!isAccepted) { + response.setBody(responseBody); + } + } else { + // If the document array is empty, query for more documents. + tryQueryAndDelete(); + } + } +} + +/** + * A DocumentDB stored procedure that updates a document by id, using a similar syntax to MongoDB's update operator.
+ *
+ * The following operations are supported:
+ *
+ * Field Operators:
+ *
    + *
  • $inc - Increments the value of the field by the specified amount.
  • + *
  • $mul - Multiplies the value of the field by the specified amount.
  • + *
  • $rename - Renames a field.
  • + *
  • $set - Sets the value of a field in a document.
  • + *
  • $unset - Removes the specified field from a document.
  • + *
  • $min - Only updates the field if the specified value is less than the existing field value.
  • + *
  • $max - Only updates the field if the specified value is greater than the existing field value.
  • + *
  • $currentDate - Sets the value of a field to current date as a Unix Epoch.
  • + *
+ *
+ * Array Operators:
+ *
    + *
  • $addToSet - Adds elements to an array only if they do not already exist in the set.
  • + *
  • $pop - Removes the first or last item of an array.
  • + *
  • $push - Adds an item to an array.
  • + *
+ *
+ * Note: Performing multiple operations on the same field may yield unexpected results.
+ * + * @example Increment the property "counter" by 1 in the document where id = "foo". + * updateSproc("foo", {$inc: {counter: 1}}); + * + * @example Set the property "message" to "Hello World" and the "messageDate" to the current date in the document where id = "bar". + * updateSproc("bar", {$set: {message: "Hello World"}, $currentDate: {messageDate: ""}}); + * + * @function + * @param {string} id - The id for your document. + * @param {object} update - the modifications to apply. + * @returns {object} the updated document. + */ +function updateSproc(id: string, update: Object) { + var collection: ICollection = getContext().getCollection(); + var collectionLink: string = collection.getSelfLink(); + var response: IResponse = getContext().getResponse(); + + // Validate input. + if (!id) throw new Error("The id is undefined or null."); + if (!update) throw new Error("The update is undefined or null."); + + tryQueryAndUpdate(); + + // Recursively queries for a document by id w/ support for continuation tokens. + // Calls tryUpdate(document) as soon as the query returns a document. + function tryQueryAndUpdate(continuation?: string) { + var query: IParameterizedQuery = { query: "select * from root r where r.id = @id", parameters: [{ name: "@id", value: id }] }; + var requestOptions: IFeedOptions = { continuation: continuation }; + + var isAccepted: boolean = collection.queryDocuments(collectionLink, query, requestOptions, function (err: IFeedCallbackError, documents: Array, responseOptions: IFeedCallbackOptions) { + if (err) throw err; + + if (documents.length > 0) { + // If the document is found, update it. + // There is no need to check for a continuation token since we are querying for a single document. + tryUpdate(documents[0]); + } else if (responseOptions.continuation) { + // Else if the query came back empty, but with a continuation token; repeat the query w/ the token. + // It is highly unlikely for this to happen when performing a query by id; but is included to serve as an example for larger queries. + tryQueryAndUpdate(responseOptions.continuation); + } else { + // Else a document with the given id does not exist.. + throw new Error("Document not found."); + } + }); + + // If we hit execution bounds - throw an exception. + // This is highly unlikely given that this is a query by id; but is included to serve as an example for larger queries. + if (!isAccepted) { + throw new Error("The stored procedure timed out."); + } + } + + // Updates the supplied document according to the update object passed in to the sproc. + function tryUpdate(document: IDocumentMeta) { + + // DocumentDB supports optimistic concurrency control via HTTP ETag. + var requestOptions: IReplaceOptions = { etag: document._etag }; + + // Update operators. + inc(document, update); + mul(document, update); + rename(document, update); + set(document, update); + unset(document, update); + min(document, update); + max(document, update); + currentDate(document, update); + addToSet(document, update); + pop(document, update); + push(document, update); + + // Update the document. + var isAccepted: boolean = collection.replaceDocument(document._self, document, requestOptions, function (err, updatedDocument, responseOptions) { + if (err) throw err; + + // If we have successfully updated the document - return it in the response body. + response.setBody(updatedDocument); + }); + + // If we hit execution bounds - throw an exception. + if (!isAccepted) { + throw new Error("The stored procedure timed out."); + } + } + + // Operator implementations. + // The $inc operator increments the value of a field by a specified amount. + function inc(document: any, update: any) { + var fields: Array, i: number; + + if (update.$inc) { + fields = Object.keys(update.$inc); + for (i = 0; i < fields.length; i++) { + if (isNaN(update.$inc[fields[i]])) { + // Validate the field; throw an exception if it is not a number (can't increment by NaN). + throw new Error("Bad $inc parameter - value must be a number") + } else if (document[fields[i]]) { + // If the field exists, increment it by the given amount. + document[fields[i]] += update.$inc[fields[i]]; + } else { + // Otherwise set the field to the given amount. + document[fields[i]] = update.$inc[fields[i]]; + } + } + } + } + + // The $mul operator multiplies the value of the field by the specified amount. + function mul(document: any, update: any) { + var fields: Array, i: number; + + if (update.$mul) { + fields = Object.keys(update.$mul); + for (i = 0; i < fields.length; i++) { + if (isNaN(update.$mul[fields[i]])) { + // Validate the field; throw an exception if it is not a number (can't multiply by NaN). + throw new Error("Bad $mul parameter - value must be a number") + } else if (document[fields[i]]) { + // If the field exists, multiply it by the given amount. + document[fields[i]] *= update.$mul[fields[i]]; + } else { + // Otherwise set the field to 0. + document[fields[i]] = 0; + } + } + } + } + + // The $rename operator renames a field. + function rename(document: any, update: any) { + var fields: Array, i: number, existingFieldName: string, newFieldName: string; + + if (update.$rename) { + fields = Object.keys(update.$rename); + for (i = 0; i < fields.length; i++) { + existingFieldName = fields[i]; + newFieldName = update.$rename[fields[i]]; + + if (existingFieldName == newFieldName) { + throw new Error("Bad $rename parameter: The new field name must differ from the existing field name.") + } else if (document[existingFieldName]) { + // If the field exists, set/overwrite the new field name and unset the existing field name. + document[newFieldName] = document[existingFieldName]; + delete document[existingFieldName]; + } else { + // Otherwise this is a noop. + } + } + } + } + + // The $set operator sets the value of a field. + function set(document: any, update: any) { + var fields: Array, i: number; + + if (update.$set) { + fields = Object.keys(update.$set); + for (i = 0; i < fields.length; i++) { + document[fields[i]] = update.$set[fields[i]]; + } + } + } + + // The $unset operator removes the specified field. + function unset(document: any, update: any) { + var fields: Array, i: number; + + if (update.$unset) { + fields = Object.keys(update.$unset); + for (i = 0; i < fields.length; i++) { + delete document[fields[i]]; + } + } + } + + // The $min operator only updates the field if the specified value is less than the existing field value. + function min(document: any, update: any) { + var fields: Array, i: number; + + if (update.$min) { + fields = Object.keys(update.$min); + for (i = 0; i < fields.length; i++) { + if (update.$min[fields[i]] < document[fields[i]]) { + document[fields[i]] = update.$min[fields[i]]; + } + } + } + } + + // The $max operator only updates the field if the specified value is greater than the existing field value. + function max(document: any, update: any) { + var fields: Array, i: number; + + if (update.$max) { + fields = Object.keys(update.$max); + for (i = 0; i < fields.length; i++) { + if (update.$max[fields[i]] > document[fields[i]]) { + document[fields[i]] = update.$max[fields[i]]; + } + } + } + } + + // The $currentDate operator sets the value of a field to current date as a POSIX epoch. + function currentDate(document: any, update: any) { + var currentDate: Date = new Date(); + var fields: Array, i: number; + + if (update.$currentDate) { + fields = Object.keys(update.$currentDate); + for (i = 0; i < fields.length; i++) { + // ECMAScript's Date.getTime() returns milliseconds, where as POSIX epoch are in seconds. + document[fields[i]] = Math.round(currentDate.getTime() / 1000); + } + } + } + + // The $addToSet operator adds elements to an array only if they do not already exist in the set. + function addToSet(document: any, update: any) { + var fields: Array, i: number; + + if (update.$addToSet) { + fields = Object.keys(update.$addToSet); + + for (i = 0; i < fields.length; i++) { + if (!Array.isArray(document[fields[i]])) { + // Validate the document field; throw an exception if it is not an array. + throw new Error("Bad $addToSet parameter - field in document must be an array.") + } else if (document[fields[i]].indexOf(update.$addToSet[fields[i]]) === -1) { + // Add the element if it doesn't already exist in the array. + document[fields[i]].push(update.$addToSet[fields[i]]); + } + } + } + } + + // The $pop operator removes the first or last item of an array. + // Pass $pop a value of -1 to remove the first element of an array and 1 to remove the last element in an array. + function pop(document: any, update: any) { + var fields: Array, i: number; + + if (update.$pop) { + fields = Object.keys(update.$pop); + + for (i = 0; i < fields.length; i++) { + if (!Array.isArray(document[fields[i]])) { + // Validate the document field; throw an exception if it is not an array. + throw new Error("Bad $pop parameter - field in document must be an array.") + } else if (update.$pop[fields[i]] < 0) { + // Remove the first element from the array if it's less than 0 (be flexible). + document[fields[i]].shift(); + } else { + // Otherwise, remove the last element from the array (have 0 default to javascript's pop()). + document[fields[i]].pop(); + } + } + } + } + + // The $push operator adds an item to an array. + function push(document: any, update: any) { + var fields: Array, i: number; + + if (update.$push) { + fields = Object.keys(update.$push); + + for (i = 0; i < fields.length; i++) { + if (!Array.isArray(document[fields[i]])) { + // Validate the document field; throw an exception if it is not an array. + throw new Error("Bad $push parameter - field in document must be an array.") + } else { + // Push the element in to the array. + document[fields[i]].push(update.$push[fields[i]]); + } + } + } + } +} + +/** + * A DocumentDB stored procedure that upserts a given document (insert new or update if present) using its id property.
+ * This implementation tries to create, and if the create fails then query for the document with the specified document's id, then replace it. + * Use this sproc if creates are more common than replaces, otherwise use "upsertOptimizedForReplace" + * + * @function + * @param {Object} document - A document that should be upserted into this collection. + * @returns {Object.} Returns an object with the property:
+ * op - created (or) replaced. + */ +function upsert(document: IDocumentMeta) { + var context: IContext = getContext(); + var collection: ICollection = context.getCollection(); + var collectionLink: string = collection.getSelfLink(); + var response: IResponse = context.getResponse(); + var errorCodes: any = { CONFLICT: 409 }; + + // Not checking for existence of document.id for compatibility with createDocument. + if (!document) throw new Error("The document is undefined or null."); + + tryCreate(document, callback); + + function tryCreate(doc: IDocumentMeta, callback: (err: IRequestCallbackError, obj: any, options: IRequestCallbackOptions) => void) { + var isAccepted: boolean = collection.createDocument(collectionLink, doc, callback); + if (!isAccepted) throw new Error("Unable to schedule create document"); + response.setBody({ "op": "created" }); + } + + // To replace the document, first issue a query to find it and then call replace. + function tryReplace(doc: IDocumentMeta, callback: (err: IRequestCallbackError, obj: any, options: IRequestCallbackOptions) => void) { + retrieveDoc(doc, null, function (retrievedDocs: Array) { + var isAccepted: boolean = collection.replaceDocument(retrievedDocs[0]._self, doc, callback); + if (!isAccepted) throw new Error("Unable to schedule replace document"); + response.setBody({ "op": "replaced" }); + }); + } + + function retrieveDoc(doc: IDocumentMeta, continuation: string, callback: Function) { + var query: IParameterizedQuery = { query: "select * from root r where r.id = @id", parameters: [{ name: "@id", value: doc.id }] }; + var requestOptions: IFeedOptions = { continuation: continuation }; + var isAccepted: boolean = collection.queryDocuments(collectionLink, query, requestOptions, function (err: IFeedCallbackError, retrievedDocs: Array, responseOptions: IFeedCallbackOptions) { + if (err) throw err; + + if (retrievedDocs.length > 0) { + callback(retrievedDocs); + } else if (responseOptions.continuation) { + // Conservative check for continuation. Not expected to hit in practice for the "id query" + retrieveDoc(doc, responseOptions.continuation, callback); + } else { + throw new Error("Error in retrieving document: " + doc.id); + } + }); + if (!isAccepted) throw new Error("Unable to query documents"); + } + + // This is called when collection.createDocument is done in order to + // process the result. + function callback(err: IRequestCallbackError, doc: any, options: IRequestCallbackOptions) { + if (err) { + // Replace the document if status code is 409 and upsert is enabled + if (err.number == errorCodes.CONFLICT) { + return tryReplace(document, callback); + } else { + throw err; + } + } + } +} + +/** + * A DocumentDB stored procedure that upserts a given document (insert new or update if present) using its id property.
+ * This implementation queries for the document's id, and creates if absent and replaces if found. + * Use this sproc if replaces are more common than creates, otherwise use "upsert" + * + * @function + * @param {Object} document - A document that should be upserted into this collection. + * @returns {Object.} Returns an object with the property:
+ * op - created (or) replaced. + */ +function upsertOptimizedForReplace(document: any) { + var context: IContext = getContext(); + var collection: ICollection = context.getCollection(); + var collectionLink: string = collection.getSelfLink(); + var response: IResponse = context.getResponse(); + + // Not checking for existence of document.id for compatibility with createDocument. + if (!document) throw new Error("The document is undefined or null."); + + retrieveDoc(document, null, callback); + + function retrieveDoc(doc: IDocumentMeta, continuation: string, callback: (err: IRequestCallbackError, obj: any, options: IRequestCallbackOptions) => void) { + var query: IParameterizedQuery = { query: "select * from root r where r.id = @id", parameters: [{ name: "@id", value: doc.id }] }; + var requestOptions: IFeedOptions = { continuation: continuation }; + var isAccepted: boolean = collection.queryDocuments(collectionLink, query, requestOptions, function (err: IFeedCallbackError, retrievedDocs: Array, responseOptions: IFeedCallbackOptions) { + if (err) throw err; + if (retrievedDocs.length > 0) { + tryReplace(retrievedDocs[0], doc, callback); + } else if (responseOptions.continuation) { + // Conservative check for continuation. Not expected to hit in practice for the "id query". + retrieveDoc(doc, responseOptions.continuation, callback); + } else { + tryCreate(doc, callback); + } + }); + if (!isAccepted) throw new Error("Unable to query documents"); + } + + function tryCreate(doc: any, callback: (err: IRequestCallbackError, obj: any, options: IRequestCallbackOptions) => void) { + var isAccepted = collection.createDocument(collectionLink, doc, callback); + if (!isAccepted) throw new Error("Unable to schedule create document"); + response.setBody({ "op": "created" }); + } + + function tryReplace(docToReplace: IDocumentMeta, docContent: any, callback: (err: IRequestCallbackError, obj: any, options: IRequestCallbackOptions) => void) { + var isAccepted = collection.replaceDocument(docToReplace._self, docContent, callback); + if (!isAccepted) throw new Error("Unable to schedule replace document"); + response.setBody({ "op": "replaced" }); + } + + function callback(err: IRequestCallbackError, obj: any, options: IRequestCallbackOptions): void { + if (err) throw err; + } +} + +/** +* This script runs as a pre-trigger when a document is inserted: +* for each inserted document, validate/canonicalize document.weekday and create field document.createdTime. +*/ +function validateClass() { + var collection: ICollection = getContext().getCollection(); + var collectionLink: string = collection.getSelfLink(); + var doc: any = getContext().getRequest().getBody(); + + // Validate/canonicalize the data. + doc.weekday = canonicalizeWeekDay(doc.weekday); + + // Insert auto-created field 'createdTime'. + doc.createdTime = new Date(); + + // Update the request -- this is what is going to be inserted. + getContext().getRequest().setBody(doc); + + function canonicalizeWeekDay(day: string) { + // Simple input validation. + if (!day || !day.length || day.length < 3) throw new Error("Bad input: " + day); + + // Try to see if we can canonicalize the day. + var days: Array = ["Monday", "Tuesday", "Wednesday", "Friday", "Saturday", "Sunday"]; + var fullDay: string; + days.forEach(function (x: string) { + if (day.substring(0, 3).toLowerCase() == x.substring(0, 3).toLowerCase()) fullDay = x; + }); + if (fullDay) return fullDay; + + // Couldn't get the weekday from input. Throw. + throw new Error("Bad weekday: " + day); + } +} + +/** +* This script runs as a trigger: +* for each inserted document, look at document.size and update aggregate properties of metadata document: minSize, maxSize, totalSize. +*/ +function updateMetadata() { + // HTTP error codes sent to our callback funciton by DocDB server. + var ErrorCode: any = { + RETRY_WITH: 449, + } + + var collection: ICollection = getContext().getCollection(); + var collectionLink: string = collection.getSelfLink(); + + // Get the document from request (the script runs as trigger, thus the input comes in request). + var doc: any = getContext().getRequest().getBody(); + + // Check the doc (ignore docs with invalid/zero size and metaDoc itself) and call updateMetadata. + if (!doc.isMetadata && doc.size != undefined && doc.size > 0) { + getAndUpdateMetadata(); + } + + function getAndUpdateMetadata() { + // Get the meta document. We keep it in the same collection. it's the only doc that has .isMetadata = true. + var isAccepted: boolean = collection.queryDocuments(collectionLink, 'SELECT * FROM root r WHERE r.isMetadata = true', function (err: IFeedCallbackError, feed: Array, options: IFeedCallbackOptions) { + if (err) throw err; + if (!feed || !feed.length) throw new Error("Failed to find the metadata document."); + + // The metadata document. + var metaDoc: any = feed[0]; + + // Update metaDoc.minSize: + // for 1st document use doc.Size, for all the rest see if it's less than last min. + if (metaDoc.minSize == 0) metaDoc.minSize = doc.size; + else metaDoc.minSize = Math.min(metaDoc.minSize, doc.size); + + // Update metaDoc.maxSize. + metaDoc.maxSize = Math.max(metaDoc.maxSize, doc.size); + + // Update metaDoc.totalSize. + metaDoc.totalSize += doc.size; + + // Update/replace the metadata document in the store. + var isAccepted: boolean = collection.replaceDocument(metaDoc._self, metaDoc, function (err: IRequestCallbackError) { + if (err) throw err; + // Note: in case concurrent updates causes conflict with ErrorCode.RETRY_WITH, we can't read the meta again + // and update again because due to Snapshot isolation we will read same exact version (we are in same transaction). + // We have to take care of that on the client side. + }); + if (!isAccepted) throw new Error("The call replaceDocument(metaDoc) returned false."); + }); + if (!isAccepted) throw new Error("The call queryDocuments for metaDoc returned false."); + } +} + +/** + * This script is meant to run as a pre-trigger to enforce the uniqueness of the "name" property. + */ + +function validateName() { + var collection: ICollection = getContext().getCollection(); + var request: IRequest = getContext().getRequest(); + var docToCreate: any = request.getBody(); + + // Reject documents that do not have a name property by throwing an exception. + if (!docToCreate.name) { + throw new Error('Document must include a "name" property.'); + } + + lookForDuplicates(); + + function lookForDuplicates(continuation?: string) { + var query: IParameterizedQuery = { + query: 'SELECT * FROM myCollection c WHERE c.name = @name', + parameters: [{ + name: '@name', + value: docToCreate.name + }] + }; + var requestOptions: IFeedOptions = { + continuation: continuation + }; + + var isAccepted: boolean = collection.queryDocuments(collection.getSelfLink(), query, requestOptions, + function (err: IFeedCallbackError, results: Array, responseOptions: IFeedCallbackOptions) { + if (err) { + throw new Error('Error querying for documents with duplicate names: ' + err.body); + } + if (results.length > 0) { + // At least one document with name exists. + throw new Error('Document with the name, ' + docToCreate.name + ', already exists: ' + JSON.stringify(results[0])); + } else if (responseOptions.continuation) { + // Else if the query came back empty, but with a continuation token; repeat the query w/ the token. + // This is highly unlikely; but is included to serve as an example for larger queries. + lookForDuplicates(responseOptions.continuation); + } else { + // Success, no duplicates found! Do nothing. + } + } + ); + + // If we hit execution bounds - throw an exception. + // This is highly unlikely; but is included to serve as an example for more complex operations. + if (!isAccepted) { + throw new Error('Timeout querying for document with duplicate name.'); + } + } +} diff --git a/documentdb-server/documentdb-server.d.ts b/documentdb-server/documentdb-server.d.ts new file mode 100644 index 00000000000000..0d902d1f06e2c4 --- /dev/null +++ b/documentdb-server/documentdb-server.d.ts @@ -0,0 +1,541 @@ +// Type definitions for DocumentDB server side JavaScript SDK +// Project: http://dl.windowsazure.com/documentDB/jsserverdocs +// Definitions by: François Nguyen +// Definitions: https://github.com/borisyankov/DefinitelyTyped/documentdb-server + +/** The Context object provides access to all operations that can be performed on DocumentDB data, as well as access to the request and response objects. */ +interface IContext { + /** Gets the collection object. */ + getCollection(): ICollection; + /** Gets the request object. */ + getRequest(): IRequest; + /** + * Gets the response object. + * Note: this is not available in pre-triggers. + */ + getResponse(): IResponse; +} + +/** + * The __ object can be used as a shortcut to the Collection and Context objects. + * It derives from the ICollection object via prototype and defines request and response properties + * which are shortcuts to getContext().getRequest() and getContext().getResponse(). + */ +interface I__Object extends ICollection { + /** Alias for getContext().getRequest() */ + request: IRequest; + /** Alias for getContext().getResponse() */ + response: IResponse; +} + +interface IQueryAPI { + /** + * Execute a filter on the input stream of documents, resulting in a subset of the input stream that matches the given filter. + * When filter is called by itself, the input document stream is the set of all documents in the current document collection. When used in a chained call, the input document stream is the set of documents returned from the previous query function. + * @param predicate The predicate function for a filter query, which acts as a truth test of whether a document should be filtered or not. + * @param options Optional query options. Should not be used in a chained call. + * @param callback Optional callback for the operation. If no callback is provided, any error in the operation will be thrown and the result document set will be written to the Response body. Should not be used in a chained call. + */ + filter(predicate: (document: Object) => boolean, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): IQueryResponse; + filter(predicate: (document: Object) => boolean, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): IQueryResponse; + /** + * Produce a new set of documents by mapping/projecting the properties of the documents in the input document stream through the given mapping predicate. + * When map is called by itself, the input document stream is the set of all documents in the current document collection. When used in a chained call, the input document stream is the set of documents returned from the previous query function. + * @param predicate The predicate function for a map/projection, which maps the input document's properties into a new document object. + * @param options Optional query options. Should not be used in a chained call. + * @param callback Optional callback for the operation. If no callback is provided, any error in the operation will be thrown and the result document set will be written to the Response body. Should not be used in a chained call. + */ + map(predicate: (document: Object) => Object, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): IQueryResponse; + map(predicate: (document: Object) => Object, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): IQueryResponse; + /** + * Produce a new set of documents by extracting a single property from each document in the input document stream. This is equivalent to a map call that projects only propertyName. + * When pluck is called by itself, the input document stream is the set of all documents in the current document collection. When used in a chained call, the input document stream is the set of documents returned from the previous query function. + * @param propertyName Name of the property to pluck from all documents in the current collection + * @param options Optional query options. Should not be used in a chained call. + * @param callback Optional callback for the operation. If no callback is provided, any error in the operation will be thrown and the result document set will be written to the Response body. Should not be used in a chained call. + */ + pluck(propertyName: string, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): IQueryResponse; + pluck(propertyName: string, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): IQueryResponse; + /** + * Flatten nested arrays from each document in the input document stream. + * When flatten is called by itself, the input document stream is the set of all documents in the current document collection. When used in a chained call, the input document stream is the set of documents returned from the previous query function. + * @param isShallow If true, flattens only the first level of nested arrays (false by default) + * @param options Optional query options. Should not be used in a chained call. + * @param callback Optional callback for the operation. If no callback is provided, any error in the operation will be thrown and the result document set will be written to the Response body. Should not be used in a chained call. + */ + flatten(isShallow?: boolean, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): IQueryResponse; + flatten(isShallow?: boolean, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): IQueryResponse; + /** + * Produce a new set of documents by sorting the documents in the input document stream in ascending order using the given predicate. + * When sortBy is called by itself, the input document stream is the set of all documents in the current document collection. When used in a chained call, the input document stream is the set of documents returned from the previous query function. + * @param predicate Predicate function defining the property to sort by. + * @param options Optional query options. Should not be used in a chained call. + * @param Optional callback for the operation. If no callback is provided, any error in the operation will be thrown and the result document set will be written to the Response body. Should not be used in a chained call. + */ + sortBy(predicate: (document: Object) => string, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): IQueryResponse; + sortBy(predicate: (document: Object) => number, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): IQueryResponse; + sortBy(predicate: (document: Object) => string, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): IQueryResponse; + sortBy(predicate: (document: Object) => number, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): IQueryResponse; + /** + * Produce a new set of documents by sorting the documents in the input document stream in descending order using the given predicate. + * When sortByDescending is called by itself, the input document stream is the set of all documents in the current document collection. When used in a chained call, the input document stream is the set of documents returned from the previous query function. + * @param predicate Predicate function defining the property to sort by. + * @param options Optional query options. Should not be used in a chained call. + * @param callback Optional callback for the operation. If no callback is provided, any error in the operation will be thrown and the result document set will be written to the Response body. Should not be used in a chained call. + */ + sortByDescending(predicate: (document: Object) => string, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): IQueryResponse; + sortByDescending(predicate: (document: Object) => number, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): IQueryResponse; + sortByDescending(predicate: (document: Object) => string, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): IQueryResponse; + sortByDescending(predicate: (document: Object) => number, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): IQueryResponse; + /** + * Terminating call for a chained query. Should be used in conjunction with the opening chain call to perform chained queries. + * When value is called, the query is queued for execution with the given options and callback. + * @param options Optional query options for the entire chained call. + * @param callback Optional callback for the operation. If no callback is provided, any error in the operation will be thrown and the result document set will be written to the Response body. + */ + value(options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): IQueryResponse; + value(options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): IQueryResponse; +} + +/** + * Stored procedures and triggers are registered for a particular collection. The Collection object supports create, read, update and delete (CRUD) and query operations on documents and attachments in the current collection. + * All collection operations are completed asynchronously. You can provide a callback to handle the result of the operation, and to perform error handling if necessary. + * Stored procedures and triggers are executed in a time-limited manner. Long-running stored procedures and triggers are defensively timed out and all transactions performed are rolled back. + * We stop queuing collection operations if the stored procedure is close to timing out. You can inspect the boolean return value of all collection operations to see if an operation was not queued and handle this situation gracefully. + */ +interface ICollection extends IQueryAPI { + /** Opening call to start a chained query. Should be used in conjunction with the closing value call to perform chained queries. */ + chain(): IQueryResponse; + + /** + * Create an attachment for the document. + * @param documentLink resource link of the collection under which the document will be created + * @param body metadata that defines the attachment media like media, contentType. It can include any other properties as part of the metedata. + * @param options optional create options + * @param callback optional callback for the operation. If no callback is provided, any error in the operation will be thrown. + */ + createAttachment(documentLink: string, + body: IAttachment, + options?: ICreateOptions, + callback?: (error: IRequestCallbackError, resources: Object, options: IRequestCallbackOptions) => void): boolean; + + /** + * Create a document under the collection. + * @param collectionLink resource link of the collection under which the document will be created + * @param body of the document. The "id" property is required and will be generated automatically if not provided (this behaviour can be overriden using the CreateOptions). Any other properties can be added. + * @param optional create options + * @param optional callback for the operation. If no callback is provided, any error in the operation will be thrown. + */ + createDocument(collectionLink: string, + body: Object, + options?: ICreateOptions, + callback?: (error: IRequestCallbackError, resources: Object, options: IRequestCallbackOptions) => void): boolean; + + /** + * Delete an attachment. + * @param attachmentLink resource link of the attachment to be deleted + * @param options optional delete options. + * @param callback optional callback for the operation. If no callback is provided, any error in the operation will be thrown. + */ + deleteAttachment(attachmentLink: string, + options?: IDeleteOptions, + callback?: (error: IRequestCallbackError, resources: Object, options: IRequestCallbackOptions) => void): boolean; + + /** + * Delete a document. + * @param documentLink resource link of the document to delete + * @param options optional delete options + * @param callback optional callback for the operation. If no callback is provided, any error in the operation will be thrown. + */ + deleteDocument(documentLink: string, + options?: IDeleteOptions, + callback?: (error: IRequestCallbackError, resources: Object, options: IRequestCallbackOptions) => void): boolean; + + /** Get alt link (name-based link) of current collection. */ + getAltLink(): string; + + /** Get self link of current collection. */ + getSelfLink(): string; + + /** + * Execute a SQL query on the attachments for the document. + * @param documentLink resource link of the document whose attachments are being queried + * @param query SQL query string. This can also be a JSON object to pass in a parameterized query along with the values. + * @param options optional query options + * @param callback optional callback for the operation. If no callback is provided, any error in the operation will be thrown. + */ + queryAttachments(documentLink: string, + query: string, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): boolean; + queryAttachments(documentLink: string, + query: IParameterizedQuery, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): boolean; + + /** + * Execute a SQL query on the documents of the collection. + * @param collectionLink resource link of the collection whose documents are being queried + * @param filterQuery SQL query string. This can also be a JSON object to pass in a parameterized query along with the values. + * @param options optional query options. + * @param callback optional callback for the operation. If no callback is provided, any error in the operation will be thrown. + */ + queryDocuments(collectionLink: string, + filterQuery: string, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): boolean; + queryDocuments(collectionLink: string, + filterQuery: string, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): boolean; + queryDocuments(collectionLink: string, + filterQuery: IParameterizedQuery, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): boolean; + queryDocuments(collectionLink: string, + filterQuery: IParameterizedQuery, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): boolean; + + /** + * Read an Attachment. + * @param attachmenLink resource link of the attachment to read + * @param options optional read options + * @param callback optional callback for the operation. If no callback is provided, any error in the operation will be thrown. + */ + readAttachment(attachmenLink: string, + options?: IReadOptions, + callback?: (error: IRequestCallbackError, resources: Object, options: IRequestCallbackOptions) => void): boolean; + + /** + * Get all attachments for the document. + * @param documentLink resource link of the document whose attachments are being read + * @param options optional read feed options + * @param callback optional callback for the operation. If no callback is provided, any error in the operation will be thrown. + */ + readAttachments(documentLink: string, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): boolean; + + /** + * Read a document. + * @param documentLink resource link of the document to read + * @param options optional read options + * @param callback optional callback for the operation. If no callback is provided, any error in the operation will be thrown. + */ + readDocument(documentLink: string, + options?: IReadOptions, + callback?: (error: IRequestCallbackError, resources: Object, options: IRequestCallbackOptions) => void): boolean; + readDocument(documentLink: string, + options?: IReadOptions, + callback?: (error: IRequestCallbackError, resources: T, options: IRequestCallbackOptions) => void): boolean; + + /** + * Get all documents for the collection. + * @param collectionLink resource link of the collection whose documents are being read + * @param options optional read feed options + * @param callback optional callback for the operation. If no callback is provided, any error in the operation will be thrown. + */ + readDocuments(collectionLink: string, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): boolean; + readDocuments(collectionLink: string, + options?: IFeedOptions, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): boolean; + + /** + * Replace an attachment. + * @param attachmentLink resource link of the attachment to be replaced + * @param attachment new attachment body + * @param options optional replace options + * @param callback optional callback for the operation. If no callback is provided, any error in the operation will be thrown. + */ + replaceAttachment(attachmentLink: string, + attachment: Object, + options?: IReplaceOptions, + callback?: (error: IRequestCallbackError, resources: Object, options: IRequestCallbackOptions) => void): boolean; + + /** + * Replace a document. + * @param documentLink resource link of the document + * @param document new document body + * @param options optional replace options + * @param callback optional callback for the operation. If no callback is provided, any error in the operation will be thrown. + */ + replaceDocument(documentLink: string, + document: Object, + options?: IReplaceOptions, + callback?: (error: IRequestCallbackError, resources: Object, options: IRequestCallbackOptions) => void): boolean; +} + +/** Options associated with a create operation. */ +interface ICreateOptions { + /** Specifies indexing directives. */ + indexAction?: string; + /** Disables automatic generation of "id" field of the document to be created (if it is not provided) */ + disableAutomaticIdGeneration?: string; +} + +/** Options associated with a delete operation. */ +interface IDeleteOptions { + /** + * The entity tag associated with the resource. + * This is matched with the persisted resource before deletion. + */ + etag?: string; +} + +/** Will contain error information if an error occurs, undefined otherwise. */ +interface IFeedCallbackError { + /** The HTTP response code corresponding to the error. */ + number: number; + /** A string containing the error information. */ + body: string; +} + +/** Information associated with the response to the operation. */ +interface IFeedCallbackOptions { + /** Opaque token for continuing the read feed or query. */ + continuation: string; + /** Comma delimited string containing the collection's current quota metrics (storage, number of stored procedure, triggers and UDFs) after completion of the operation. */ + currentCollectionSizeInMB: string; + /** Comma delimited string containing the collection's maximum quota metrics (storage, number of stored procedure, triggers and UDFs). */ + maxCollectionSizeInMB: string; +} + +/** Options associated with a read feed or query operation. */ +interface IFeedOptions { + /** + * Max number of items to be returned in the enumeration operation. + * Value is 100 by default + */ + pageSize?: number; + /** Opaque token for continuing the enumeration. */ + continuation?: string; + /** Allow scan on the queries which couldn't be served as indexing was opted out on the requested paths (only for queryDocuments() and queryAttachments()) */ + enableScan?: boolean; + /** Allow order by with low precision (only for queryDocuments(), sortBy() and sortByDescending()) */ + enableLowPrecisionOrderBy?: boolean; +} + +/** + * Object returned from a query function, namely chain, filter, map, pluck, flatten, or value. + * If the query is part of a chained call, then this object can be used to chain further queries until the final terminating value call. + */ +interface IQueryResponse extends IQueryAPI { + /** True if the query has been queued, false if it is not queued because of a pending timeout. */ + isAccepted: boolean; +} + +/** Options associated with a read operation. */ +interface IReadOptions { + /** The conditional HTTP method ifNoneMatch value. */ + ifNoneMatch?: string; +} + +/** Options associated with a replace operation. */ +interface IReplaceOptions { + /** Specifies indexing directives. */ + indexAction?: string; + /** + * The entity tag associated with the resource. + * This is matched with the persisted resource before deletion. + */ + etag?: string; +} + +/** Will contain error information if an error occurs, undefined otherwise. */ +interface IRequestCallbackError { + /** The HTTP response code corresponding to the error. */ + number: number; + /** A string containing the error information. */ + body: string; +} + +/** Information associated with the response to the operation. */ +interface IRequestCallbackOptions { + /** Comma delimited string containing the collection's current quota metrics (storage, number of stored procedure, triggers and UDFs) after completion of the operation. */ + currentCollectionSizeInMB: string; + /** Comma delimited string containing the collection's maximum quota metrics (storage, number of stored procedure, triggers and UDFs). */ + maxCollectionSizeInMB: string; + /** Set to true if the requested resource has not been modified compared to the provided ETag in the ifNoneMatch parameter for a read request. */ + notModified: boolean; +} + +interface IAttachment extends Object { + /** MIME contentType of the attachment */ + contentType: string; + /** media link associated with the attachment content */ + media: string; +} + +interface IDocumentMeta extends Object { + id: string; + _self: string; + _ts: string; + _rid?: string; + _etag?: string; + _attachments?: string; +} + +/** + * The Request object represents the request message that was sent to the server. This includes information about HTTP headers and the body of the HTTP request sent to the server. + * For triggers, the request represents the operation that is executing when the trigger is run. For example, if the trigger is being run ("triggered") on the creation of a document, then + * the request body contains the JSON body of the document to be created. This can be accessed through the request object and (as JSON) can be natively consumed in JavaScript. + * For stored procedures, the request contains information about the request sent to execute the stored procedure. + */ +interface IRequest { + /** + * Gets the request body. + */ + getBody(): Object; + getBody(): T; + /** + * Gets a specified request header value. + * @param key the name of the header to retrieve + */ + getValue(key: string): string; + /** + * Sets the request body. + * Note: this can be only used in a pre-trigger to overwrite the existing request body. + * The overwritten request body will then be used in the operation associated with this pre-trigger. + * @param value the value to set in the request body + */ + setBody(value: string): void; + setBody(value: Object): void; + /** + * Sets a specified request header value. + * Note: this method cannot be used to create new headers. + * @param key the name of the header + * @param value the value of the header + */ + setValue(key: string, value: string): void; + + appendBody(value: string): void; + appendBody(value: Object): void; +} + +/** + * The Response object represents the response message that will be sent from the server in response to the requested operation. This includes information about the HTTP headers and body of the response from the server. + * The Response object is not present in pre-triggers because they are run before the response is generated. + * For post-triggers, the response represents the operation that was executed before the trigger. For example, if the post-trigger is being run ("triggered") after the creation of a document, then + * the response body contains the JSON body of the document that was created. This can be accessed through the response object and (as JSON) can be natively consumed in JavaScript. + * For stored procedures, the response can be manipulated to send output back to the client-side. + * Note: this object not available in pre-triggers + */ +interface IResponse { + /** + * Gets the response body. + */ + getBody(): Object; + getBody(): T; + /** + * Gets a maximum quota allowed for the resource associated with a post-trigger + * Note: this method is only available in post-triggers + */ + getMaxResourceQuota(): string; + /** + * Gets a current quota usage for the resource associated with a post-trigger + * Note: this method is only available in post-triggers + */ + getResourceQuotaCurrentUsage(): string; + /** + * Gets a specified response header value. + * @param key the name of the header to retrieve + */ + getValue(key: string): string; + /** + * Sets the response body. + * Note: This cannot be done in pre-triggers. + * In post-triggers, the response body is already set with the requested resource and will be overwritten with this call. + * In stored procedures, this call can be used to set the response message body as output to the calling client. + */ + setBody(value: string): void; + setBody(value: Object): void; + /** + * Sets a specified response header value. + * Note: this method cannot be used to create new headers. + * @param key the name of the header + * @param value the value of the header + */ + getValue(key: string, value: string): void; + + appendBody(value: string): void; + appendBody(value: Object): void; +} + +/** Can be used as the query parameter in queryAttachments and queryDocuments. */ +interface IParameterizedQuery { + /** SQL query string. */ + query: string; + /** Parameters */ + parameters: Array; +} + +/** Parameter interface for parameterized queries */ +interface IQueryParam { + /** Name to use in the query */ + name: string; + /** Value of the parameter */ + value: string; +} + +/** List of error codes returned by database operations in the RequestCallback and FeedCallback. See the corresponding error message for more details. */ +interface IErrorCodes { + // Client error + /** (400) Request failed due to bad inputs **/ + BadRequest: number; + /** (403) Request was denied access to the resource **/ + Forbidden: number; + /** (404) Request tried to access a resource which doesn't exist **/ + NotFound: number; + /** (409) Resource with the specified id already exists **/ + Conflict: number; + /** (412) Conditions specified in the request options were not met **/ + PreconditionFailed: number; + /** (413) Request failed because it was too large **/ + RequestEntityTooLarge: number; + /** (449) Request conflicted with the current state of a resource and must be retried from a new transaction from the client side **/ + RetryWith: number; + // Server error + /** (500) Server encountered an unexpected error in processing the request **/ + InternalServerError: number; +} + +declare function getContext(): IContext; +declare var __: I__Object; +declare var ErrorCodes: IErrorCodes; \ No newline at end of file