DEV Community

Stanislaw Baranski
Stanislaw Baranski

Posted on • Edited on

Writing Mongo Realm serverless functions with TypeScript

Mongo Realm serverless functions don't support TypeScript natively, this article describes how to create an environment that allows writing functions using TypeScript.

The process of enabling TS support consist of the following steps:

  1. πŸ–² Moving configuration files to Github repository
  2. πŸ— Creating a TypeScript build chain
  3. πŸ”© Add type declarations
  4. 🚜 Enabling Github Automatic Deployments
  5. πŸ• Enforce consistency with husky hooks
  6. πŸ§ͺ (Optional) Add CI/CD workflow using Github Actions

Mongo Realm doesn't support TypeScript natively, and we don't want to change that. Instead, we leverage the beauty of TypeScript––the fact that TS produces JavaScript files during the transpilation process. That output JS files can be easily consumed by Mongo Realm. In other words, adding support for TypeScript comes down to structuring our codebase, and adding one extra step to our build chain.

πŸ–² Moving configuration files to Github repository

Mongo Realm allows exporting/importing all project configuration files, which is great for versioning and collaborative development. To automate the process, we can use Github Automatic Deployment to sync our codebase with the Mongo Realm application.

First, we need to create a Github repo (we use handy gh cli tool, but you can create it manually on Github website)

gh repo create <name-of-the-repo>
cd <name-of-the-repo>
Enter fullscreen mode Exit fullscreen mode

Now, make sure you have installed and authenticated real-cli, if not, follow these steps.

# download your realm app configuration files
realm-cli export \
 --app-id <realm-app-id> \ 
 --output myRealmApp \
 --for-source-control

# move downloaded content and remove empty directory
mv myRealmApp/* . && rm -r myRealmApp

# commit all changes to GitHub repo
git add .
git commit -m "initial commit"
git push
Enter fullscreen mode Exit fullscreen mode

πŸ— Creating TypeScript buildchain

Now we need to add a buildchain for producing JavaScript files from our TypeScript sources. We create a src/ directory that keeps our TypeScript version of the project source files.

It's important to match the directories structure of Mongo Realm application,

mkdir src
cp -r functions src
Enter fullscreen mode Exit fullscreen mode

If we want to create a function called testFunc we will create a directory src/functions/testFunc with source.ts and config.json files.

// src/functions/testFunc/source.ts
exports = (test: string) => {
  return test.toUpperCase()
}

// src/functions/testFunc/config.json
{
  "name": "testFunc",
  "private": false
}
Enter fullscreen mode Exit fullscreen mode

Now, we need to transpile these source files to JavaScript equivalent. We create a Nodejs project at a project root level and add typescript packages.

npm init -y
npm i -D typescript @types/node
npx tsc --init
Enter fullscreen mode Exit fullscreen mode

In tsconfig.json we specify

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "./",
    "rootDir": "./src",
    "resolveJsonModule": true,
    "moduleResolution": "node",
  },
  "include": ["src/**/*", "src/**/*.json"],
}
Enter fullscreen mode Exit fullscreen mode

Mongo Realm supports ES6+ so we set the target accordingly. Merging functions from src to root level is achieved with outDir and rootDir. Resolving JSONs is required because we want our config.json files to be copied from src to outDir directory.

Now we are ready to execute transpilation with npx tsc

$ tree functions

functions
└── testFunc
    β”œβ”€β”€ config.json
    └── source.js

$ cat functions/testFunc/source.js

exports = function (test) {
    return test.toUpperCase();
};
Enter fullscreen mode Exit fullscreen mode

At this point, our JavaScript function is generated and ready to be deployed.

πŸ”© Add type declarations

Realm does not allow us to use ES Modules––therefore we can not import types, what we can do instead is to create type declarations that are automatically consumed by TypeScript compiler.

For instance, to add support for global context object, create /src/types/realm.d.ts file with the following content

/// <reference types="mongodb" />

type Services = {
  (name: "mongodb-atlas") : import("mongodb").MongoClient;
  // add your other services here
  (name: string) : any;
}
declare global {
   namespace context {
      const services: {
         get: Services;
      };
      const user: {
         id: string;
         type: "normal" | "server" | "system";
         data: object;
         custom_data: object;
         identities: any[];
      };
      const functions: {
         execute: (name: string, ...args: any[]) => any;
      };
      const environment: {
         tag: string;
      };
      const values: {
         get: (value) => any;
      };
   }
}
Enter fullscreen mode Exit fullscreen mode

Instead of using import syntax, we use the /// <reference directive for importing mongodb type declarations.

Lastly, install mongodb package

npm i mongodb
Enter fullscreen mode Exit fullscreen mode

And enjoy the strict typing of context object.

// src/functions/testFunc/source.ts
exports = (test: string) => {
  const client: MongoClient = context.services.get("mongo-atlas")
  // TypeScript correctly detect MongoClient type.
  return test.toUpperCase()
}
Enter fullscreen mode Exit fullscreen mode

🚜 Enabling Github Automatic Deployments

We want our Github repo to be the single source of truth, to achieve that, we need Realm to sync with our repo. Fortunately, it's easy with the Github Automatic Deployments feature.Β 
Now each time, you make a push to your repo, the changes will be automatically deployed to your Realm application.

πŸ• Enforce consistency with husky hooks

Right now, you have to remember to transpile your TypeScript source code before each push to the Github repo. If you forget about it, the changes won't take effect, since your /functions directory contains out-dated JavaScript files. Luckily, husky comes to the rescue––it allows to set hook actions that will be triggered before each commit/push.

npm i -D husky
npx husky install
npx husky add pre-commit "npx tsc && git add functions"
Enter fullscreen mode Exit fullscreen mode

Now each time we execute git commit, husky will execute tsc for us, making sure that the src/ and /functions are synchronized before pushing a change to the repo––and so, deploying to Realm app.

πŸ§ͺ (Optional) Add CI/CD workflow using Github Actions

As the icing on the cake, we can add CI/CD automation workflow. Since there is already a great resource on how to achieve it, I will just leave the link to the repo https://github.com/mongodb-developer/SocialStats.

I personally prefer Github Actions instead of Jenkins, if you are interested in achieving the same workflow using GH Actions, please let me know in the comments, and I will extend this article.

πŸ§‘β€πŸ« Conclusions

The Mongo Realm serverless functions infrastructure is still in the early stage, in contrast to e.g. Firebase (Google Cloud Functions) that supports TypeScript functions natively. Yet, in this article, we've shown that by preparing the development environment it is possible.
Personally, I believe that in the future realm-cli will handle this whole process for us (similarly how firebase cli does).

I hope this article was useful and saved you some time. πŸ‘‹

Top comments (5)

Collapse
 
secularmonk profile image
Calum Craig

This is a great solution and helped me a lot. However, I had difficulty with one part. I'm new to typescript so others may not struggle as much, but thought it could help someone.

When adding type declarations in the d.ts file, I found they weren't recognised using the exact code in the Add type declarations section. I needed to declare them as a global namespace to fix it.

So instead of this:

declare namespace context {
  const services: {
    get: Services;
  };
...
Enter fullscreen mode Exit fullscreen mode

I used this:

declare global {
   namespace context {
      const services: {
         get: Services;
      };
...
Enter fullscreen mode Exit fullscreen mode

Since it's declared globally, the context variables are recognised by the compiler. In my project I also use the context.environment.tag and context.values features, so I added in handling for these as well within the global context namespace, as below.

...
const environment: {
         tag: string;
      };
      const values: {
         get: (value) => any;
      };
...
Enter fullscreen mode Exit fullscreen mode

Hope this helps someone. Thanks Stanislaw for your article, saved me a massive headache.

Collapse
 
stanbar profile image
Stanislaw Baranski

Thank you, I've added your improvements :)

Collapse
 
dimaip profile image
Dmitri Pisarev πŸ‡·πŸ‡Ί

Thanks for sharing!
However manually providing type definitions is a nightmare, especially things like cluster.db('x').collection('x').findOneAndUpdate... You can't even borrow definitions from mongodb node driver because they are slightly different here and there :(

Collapse
 
stanbar profile image
Stanislaw Baranski

I assume it's something you have already figured out yourself, but I've added the "πŸ”© Add type declarations" subsection, it's not perfect but isn't that bad either.

Collapse
 
alanxtreme profile image
Alan Daniel

Cannot find name 'context'.ts(2304)