From d03ff75ee65ed6e715bd69e4d018f4823c5f6f2d Mon Sep 17 00:00:00 2001 From: johndowns Date: Sat, 13 Jan 2018 05:18:49 +1100 Subject: [PATCH] Update DocumentDB Server Types with new Cosmos DB features add missing type parameters on some overloads, and revise test cases (#22605) * 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 --- .../documentdb-server-tests.ts | 294 +----------------- types/documentdb-server/index.d.ts | 52 +++- 2 files changed, 52 insertions(+), 294 deletions(-) diff --git a/types/documentdb-server/documentdb-server-tests.ts b/types/documentdb-server/documentdb-server-tests.ts index cc7c97e07c..71cb482c09 100644 --- a/types/documentdb-server/documentdb-server-tests.ts +++ b/types/documentdb-server/documentdb-server-tests.ts @@ -1,6 +1,6 @@ -// Samples taken from http://dl.windowsazure.com/documentDB/jsserverdocs/Collection.html +// Samples taken from http://azure.github.io/azure-documentdb-js-server/Collection.html function chain() { var name: string = "John"; var result: IQueryResponse = __.chain() @@ -228,118 +228,6 @@ function count(filterQuery: string, continuationToken: string) { } } -/** -* 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: @@ -445,6 +333,8 @@ function bulkDeleteSproc(query: string) { } } +// 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.
*
@@ -750,130 +640,6 @@ function updateSproc(id: string, update: Object) { } } -/** - * 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. @@ -963,56 +729,4 @@ function updateMetadata() { } } -/** - * 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.'); - } - } -} +// 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. diff --git a/types/documentdb-server/index.d.ts b/types/documentdb-server/index.d.ts index 974c0661fa..dad0bad292 100644 --- a/types/documentdb-server/index.d.ts +++ b/types/documentdb-server/index.d.ts @@ -1,9 +1,9 @@ -// Type definitions for DocumentDB server side JavaScript SDK -// Project: http://dl.windowsazure.com/documentDB/jsserverdocs -// Definitions by: François Nguyen +// Type definitions for Cosmos DB server-side JavaScript SDK +// Project: http://azure.github.io/azure-documentdb-js-server/ +// Definitions by: François Nguyen , John Downs // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -/** 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. */ +/** The Context object provides access to all operations that can be performed on Cosmos DB data, as well as access to the request and response objects. */ interface IContext { /** Gets the collection object. */ getCollection(): ICollection; @@ -227,6 +227,9 @@ interface ICollection extends IQueryAPI { queryDocuments(collectionLink: string, filterQuery: string, callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): boolean; + queryDocuments(collectionLink: string, + filterQuery: string, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): boolean; queryDocuments(collectionLink: string, filterQuery: string, options?: IFeedOptions, @@ -238,6 +241,9 @@ interface ICollection extends IQueryAPI { queryDocuments(collectionLink: string, filterQuery: IParameterizedQuery, callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): boolean; + queryDocuments(collectionLink: string, + filterQuery: IParameterizedQuery, + callback?: (error: IFeedCallbackError, resources: Array, options: IFeedCallbackOptions) => void): boolean; queryDocuments(collectionLink: string, filterQuery: IParameterizedQuery, options?: IFeedOptions, @@ -330,6 +336,36 @@ interface ICollection extends IQueryAPI { document: Object, options?: IReplaceOptions, callback?: (error: IRequestCallbackError, resources: Object, options: IRequestCallbackOptions) => void): boolean; + + /** + * Upsert an attachment for the document. + * @param documentLink resource link of the document under which the attachment will be upserted + * @param body metadata that defines the attachment media like media, contentType. It can include any other properties as part of the metadata. + * @param options optional upsert options + * @param callback optional callback for the operation. If no callback is provided, any error in the operation will be thrown. + */ + upsertAttachment(documentLink: string, + body: Object, + callback?: (error: IRequestCallbackError, resources: Object, options: IRequestCallbackOptions) => void): boolean; + upsertAttachment(documentLink: string, + body: Object, + options?: IUpsertOptions, + callback?: (error: IRequestCallbackError, resources: Object, options: IRequestCallbackOptions) => void): boolean; + + /** + * Upsert a document under the collection. + * @param collectionLink resource link of the collection under which the document will be upserted + * @param body body of the document. The "id" property is required and will be generated automatically if not provided (this behaviour can be overriden using the UpsertOptions). Any other properties can be added. + * @param options optional upsert options + * @param callback optional callback for the operation. If no callback is provided, any error in the operation will be thrown. + */ + upsertDocument(collectionLink: string, + body: Object, + callback?: (error: IRequestCallbackError, resources: Object, options: IRequestCallbackOptions) => void): boolean; + upsertDocument(collectionLink: string, + body: Object, + options?: IUpsertOptions, + callback?: (error: IRequestCallbackError, resources: Object, options: IRequestCallbackOptions) => void): boolean; } /** Options associated with a create operation. */ @@ -541,6 +577,14 @@ interface IQueryParam { value: any; } +/** Options associated with a upsert operation. */ +interface IUpsertOptions { + /** Specifies indexing directives. */ + indexAction?: string; + /** Disables automatic generation of "id" field of the document to be upserted (if it is not provided) */ + disableAutomaticIdGeneration?: 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