Matteo Bortolazzo
Posted on November 6, 2022
Server-side programming
An underestimated aspect of Cosmos DB is its server-side components:
- Stored procedures
- Triggers
- User-defined functions (UDFs)
These are all JavaScript functions that run natively in Cosmos DB's query engine (close to Edge 18).
Stored procedures
These are close to the ones we find in RDMSs run in a transactional context with a logical partition. We can read and write documents within the partition. If the operation throws any exception, the engine rolls back the changes.
Triggers
Triggers are functions that we can run before or after specific document operations.
For example, we can:
- Add properties to a request before the document is created.
- Only allow the update of a document if the request is correct.
- Update a second document (within the same partition) after the document is updated.
- Rollback document changes if a post-trigger fails some checks.
User-defined functions
UDFs are functions we can use in queries with clauses like SELECT
or WHERE
. We can take full advantage of a modern JavaScript engine and its APIs.
The JavaScript experience
Most of us are used to types. Types self-document the code, and with the help of IDEs, they make development quicker.
Cosmos DB has no official TypeScript type definitions. So, when I started working on the stored procedures, I quickly felt the need for them. Mainly because there are just a bunch of examples and the official JDocs takes a lot of work to consume.
function createToDoItems(items) {
var collection = getContext().getCollection();
var collectionLink = collection.getSelfLink();
var count = 0;
if (!items) throw new Error("The array is undefined or null.");
var numItems = items.length;
if (numItems == 0) {
getContext().getResponse().setBody(0);
return;
}
tryCreate(items[count], callback);
function tryCreate(item, callback) {
var options = { disableAutomaticIdGeneration: false };
var isAccepted = collection.createDocument(collectionLink, item, options, callback);
if (!isAccepted) getContext().getResponse().setBody(count);
}
function callback(err, item, options) {
if (err) throw err;
count++;
if (count >= numItems) {
getContext().getResponse().setBody(count);
} else {
tryCreate(items[count], callback);
}
}
}
For these reasons, I converted the official JDocs to TypeScript type definitions and published an NPM package, azure-cosmosdb-js-server-types
Use types
Cosmos DB requires functions to be self-contained. We cannot reference external packages and import types from them. We need to import types during builds.
Run npm install azure-cosmosdb-js-server-types
, then update the tsconfig.json
file to include the new types:
{
...
"compilerOptions": {
...
"types": [ "azure-cosmosdb-js-server-types" ]
}
}
Now we can use getContext
or __
without any import:
interface User {
name: string;
age: number;
addresses: Address[];
}
interface Address {
city: string;
}
function runQuery() {
const result = __.chain<User>()
.filter(doc => doc.age > 30)
.sortBy(user => user.age)
.map(user => user.addresses)
.flatten<Address>()
.value(null, callback)
if (!result.isAccepted)
throw new Error("The call was not accepted");
function callback(err: Error, items: Address[]) {
if (err) throw err;
// or getContext().getResponse().setBody({
__.response.setBody({
result: items
})
}
}
Compile it in .NET builds
If we are a .NET developer, depending on the use case, we might want to compile scripts when building APIs or console applications. Luckily, it's pretty easy.
-
Add a
package.json
file in the project folder with dependencies to TypeScript and the type definitions:
{ "scripts": { "build": "tsc" }, "dependencies": { "azure-cosmosdb-js-server-types": "^1.0.0", "typescript": "^4.8.4" } }
We also add a script to compile TypeScript with the
tsc
command: -
Add a
tsconfig.json
file in the project folder. Here we tell TypeScript how it should compile.
{ "compileOnSave": true, "compilerOptions": { "noImplicitAny": false, "noEmitOnError": true, "removeComments": false, "sourceMap": false, "target": "es5", "types": [ "azure-cosmosdb-js-server-types" ] }, "include": [ "./Scripts/**/*" ] }
In the
include
section we specify the folder where the TypeScript files are located. In the
csproj
file we need to add a step to install the dependencies:
<Target Name="NpmInstall" Inputs="package.json" Outputs="node_modules/.install-stamp">
<Exec Command="npm ci" Condition="'$(RestorePackagesWithLockFile)' == 'true'" />
<Exec Command="npm install" Condition="'$(RestorePackagesWithLockFile)' != 'true'" />
<Touch Files="node_modules/.install-stamp" AlwaysCreate="true" />
</Target>
This code installs packages only if needed.
- Finally, we add a step to compile the TypeScript files:
<Target Name="NpmRunBuild" DependsOnTargets="NpmInstall" BeforeTargets="BeforeBuild">
<Exec Command="npm run build -- --outDir $(OutDir)Scripts" />
</Target>
The JavaScript files will be generated in a Scripts
folder in the project's output folder, e.g. bin\Debug\net6.0
. We use the --outDir
option to specify the output folder and $(OutDir)
to get the project's output folder.
Final words
You can find a working example on GitHub https://github.com/matteobortolazzo/azure-cosmosdb-js-server-types-example.
I hope this article has been helpful to you. If you have any questions or suggestions, please leave a comment below.
Posted on November 6, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.