-
Notifications
You must be signed in to change notification settings - Fork 0
/
cosmos_partial_update.py
308 lines (269 loc) · 12.3 KB
/
cosmos_partial_update.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
SPROC_NAME="partialUpdate"
SPROC_BODY = """
/**
* 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, update) {
var collection = getContext().getCollection();
var collectionLink = collection.getSelfLink();
var response = 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) {
var query = {query: "select * from root r where r.id = @id", parameters: [{name: "@id", value: id}]};
var requestOptions = {continuation: continuation};
var isAccepted = collection.queryDocuments(collectionLink, query, requestOptions, function (err, documents, responseOptions) {
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) {
// DocumentDB supports optimistic concurrency control via HTTP ETag.
var requestOptions = {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 = 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, update) {
var fields, i;
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, update) {
var fields, i;
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, update) {
var fields, i, existingFieldName, newFieldName;
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, update) {
var fields, i;
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, update) {
var fields, i;
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, update) {
var fields, i;
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, update) {
var fields, i;
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, update) {
var currentDate = new Date();
var fields, i;
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, update) {
var fields, i;
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, update) {
var fields, i;
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, update) {
var fields, i;
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]]);
}
}
}
}
}
"""