mirror of
https://github.com/gosticks/DefinitelyTyped.git
synced 2025-10-16 12:05:41 +00:00
* Update metadata * Add IUpsertOptions interface * Add upsertAttachment method overloads * Add upsertDocument method overloads * Update test metadata * Remove old stored procedure samples from tests * Add comments for extra tests that cannot currently be added * Update author name format to match convention * Add missing overloads to ICollection.queryDocuments with type parameters
733 lines
33 KiB
TypeScript
733 lines
33 KiB
TypeScript
|
|
|
|
// Samples taken from http://azure.github.io/azure-documentdb-js-server/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<any>, 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<any>, 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<Object>) {
|
|
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<any>, 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:
|
|
* - 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<any>, 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.<br/>
|
|
* 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.<number, boolean>} Returns an object with the two properties:<br/>
|
|
* deleted - contains a count of documents deleted<br/>
|
|
* 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<any>, 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<IDocumentMeta>) {
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
// NOTE: the sample `sum` stored procedure (https://github.com/Azure/azure-documentdb-js-server/blob/master/samples/stored-procedures/sum.js) does not currently work, because it appears to throw an invalid Error object. See https://github.com/Azure/azure-documentdb-js-server/issues/23 to track this issue.
|
|
|
|
/**
|
|
* A DocumentDB stored procedure that updates a document by id, using a similar syntax to MongoDB's update operator.<br/>
|
|
* <br/>
|
|
* The following operations are supported:<br/>
|
|
* <br/>
|
|
* Field Operators:<br/>
|
|
* <ul>
|
|
* <li>$inc - Increments the value of the field by the specified amount.</li>
|
|
* <li>$mul - Multiplies the value of the field by the specified amount.</li>
|
|
* <li>$rename - Renames a field.</li>
|
|
* <li>$set - Sets the value of a field in a document.</li>
|
|
* <li>$unset - Removes the specified field from a document.</li>
|
|
* <li>$min - Only updates the field if the specified value is less than the existing field value.</li>
|
|
* <li>$max - Only updates the field if the specified value is greater than the existing field value.</li>
|
|
* <li>$currentDate - Sets the value of a field to current date as a Unix Epoch.</li>
|
|
* </ul>
|
|
* <br/>
|
|
* Array Operators:<br/>
|
|
* <ul>
|
|
* <li>$addToSet - Adds elements to an array only if they do not already exist in the set.</li>
|
|
* <li>$pop - Removes the first or last item of an array.</li>
|
|
* <li>$push - Adds an item to an array.</li>
|
|
* </ul>
|
|
* <br/>
|
|
* Note: Performing multiple operations on the same field may yield unexpected results.<br/>
|
|
*
|
|
* @example <caption>Increment the property "counter" by 1 in the document where id = "foo".</caption>
|
|
* updateSproc("foo", {$inc: {counter: 1}});
|
|
*
|
|
* @example <caption>Set the property "message" to "Hello World" and the "messageDate" to the current date in the document where id = "bar".</caption>
|
|
* 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<any>, 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<string>, 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<string>, 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<string>, 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]; // tslint:disable-line no-dynamic-delete
|
|
} else {
|
|
// Otherwise this is a noop.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The $set operator sets the value of a field.
|
|
function set(document: any, update: any) {
|
|
var fields: Array<string>, 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<string>, i: number;
|
|
|
|
if (update.$unset) {
|
|
fields = Object.keys(update.$unset);
|
|
for (i = 0; i < fields.length; i++) {
|
|
delete document[fields[i]]; // tslint:disable-line no-dynamic-delete
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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<string>, 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<string>, 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<string>, 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<string>, 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<string>, 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<string>, 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]]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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<string> = ["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<any>, 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.");
|
|
}
|
|
}
|
|
|
|
// NOTE: the sample `uniqueConstraint` trigger (https://github.com/Azure/azure-documentdb-js-server/blob/master/samples/triggers/uniqueConstraint.js) does not currently work, because it appears to use a method that does not exist on `Request`, and also appears to throw an invalid Error object. See https://github.com/Azure/azure-documentdb-js-server/issues/22 and https://github.com/Azure/azure-documentdb-js-server/issues/23 to track these issues.
|