Create a sproc to atomically update a document

Create a sproc to atomically update a document

Overview

Why stored procedure?

The Cosmos SQL is very flexable, but the client cannot easily guarantee an atomic update. It is possible to get a similar effect using the newer .Update method in the SDK along with a conditional header using the doc._etag. But sometimes a stored procedure makes more sense.

Use Case

Imagine we have documents with a counter field and we want to incement it by one every time it is called. If we download the document, add 1, then post it back we run the risk of missing an update.

The Code

We will building on the previous but start to use Promises to simplify the sproc code.

Lets start with out test runner

 1async function run() {
 2  console.log("\n*****\n* Starting test case\n")
 3  await createOrUpdateSproc(updateSproc)
 4
 5  const fixtures = getFixtures(guid())
 6
 7  //write a doc to cosmos for the sproc to edit  
 8  const doc = await createDoc(fixtures.userDoc)
 9
10  //user_id is the partition key for this particular collection
11  const docId = doc.id
12  const partitionKey = doc.user_id
13
14  // runSproc(sproc name, partition key, document id to )
15  const sprocRes = await runSproc(updateSproc.id, partitionKey, docId)
16  // console.log("sprocRes :", sprocRes)
17
18  const newDoc = await getDoc(dId, partitionKey)
19  assert.strictEqual(newDoc.touched, 1, "touched should equal 1")
20  await deleteDoc(fixtures.userDoc)
21
22  console.log("\n*Assertions passed")
23  return newDoc
24}
25
26run()
27  .then(console.log)
28  .catch(console.error)

Fixture generator

We only need a single document for this test.

 1function guid() {
 2  return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (a) =>
 3    (a ^ ((Math.random() * 16) >> (a / 4))).toString(16)
 4  );
 5}
 6
 7function getFixtures(guid) {
 8  return {
 9    userDoc: {
10      id: "user-" + guid,
11      user_id: "test-user",
12      coupon_name: "sproc_testing",
13      user_details: "I am the user doc",
14      detail: "child detail"
15    }
16  }
17}

the stored procedure

Since the sprocs use callbacks we would have to triple nest this.

  1. Get the document
  2. update the document
  3. replace the document

To make this javascipt a bit more clear, lets use promises and frame out someething like this.

 1const updateSproc = {
 2    id: "updateSproc_001",
 3    body: function (docId) {
 4      console.log("Sproc called with " + docId)
 5
 6    getDocument(__.getAltLink() + '/docs/' + docId)
 7        .then(updateDoc)
 8        .then(replace)
 9        .then(setResponse)
10        .catch(setResponse)

in order to get there lets start simply with a sproc that just retrieves the document. It does not make the update.

 1const updateSproc = {
 2    id: "updateSproc_001",
 3    body: function (docId) {
 4      console.log("Sproc called with " + docId)
 5
 6    getDocument(__.getAltLink() + '/docs/' + docId)
 7        .then(setResponse)
 8        .catch(setResponse)
 9
10      function getDocument(documentLink) {
11        return new Promise( (resolve, reject) =>{
12          var isAccepted = __.readDocument(documentLink, {}, function (err, feed, options) { 
13            console.log("feed ", feed, err, "]")
14                if (err) reject(err)
15                resolve(feed)
16            })
17            if (!isAccepted) reject('The query was not accepted by the server.')
18          })
19        }
20
21      function setResponse(body) {
22        getContext().getResponse().setBody(body)
23      }
24    }
25}

At this point you may have a failing test (because doc.touched will be undefined). Take some time to make this stable so that you can start iterating on the stored prod.

readDocument

1var isAccepted = __.readDocument(documentLink, {}, function (err, feed, options) { 
2  console.log("feed ", feed, err, "]")
3  if (err) reject(err)
4  resolve(feed)
5})

__ is a shorthand for getContext().getCollection()

readDocument takes a documentLink, request options and a callback.

The documentLink is a name based link. In the early days of Comsos links to the collection and documents were based on IDs such as dbs/6kJfAA==/colls/6kJfAOyb9Zw=/docs/6kJfAOyb9ZwyAQAAAAAAAA==/ In the current version the links are human readable based on the collection and database names such as dbs/goCart/colls/user_coupons/docs/user-46f28052-d2f4-4269-ac11-0fd736481662 the new version is much more clear, but is condered the 'alternative link'. The __self and getSelfLink() return the legacy link.

Finish the code

At this point you should have simple script that represents a failing test. It should be fast and you can iterate to explore how the sproc works.

Full Working Script

Working sproc

 1const updateSproc = {
 2    id: "updateSproc_001",
 3    body: function (docId) {
 4      console.log("Sproc called with " + docId)
 5
 6    getDocument(__.getAltLink() + '/docs/' + docId)
 7        .then(updateDoc)
 8        .then(replace)
 9        .then(setResponse)
10        .catch(setResponse)
11
12      function updateDoc(doc) {
13          doc.touched = (doc.touched || 0) + 1
14          return doc
15      }  
16
17      function replace(doc) { 
18        const documentLink = __.getAltLink() + '/docs/' + doc.id
19        return  new Promise( (resolve,reject) => {
20                __.replaceDocument(documentLink, doc, function(err, feed){
21                if (err) reject(err)
22                resolve(doc)
23              })
24          })
25      }
26
27      function getDocument(documentLink) {
28        return new Promise( (resolve, reject) =>{
29          var isAccepted = __.readDocument(documentLink, {}, function (err, feed, options) { 
30            console.log("feed ", feed, err, "]")
31                if (err) reject(err)
32                resolve(feed)
33            })
34            if (!isAccepted) reject('The query was not accepted by the server.')
35          })
36        }
37
38      function setResponse(body) {
39        getContext().getResponse().setBody(body)
40      }
41    }
42}