Making Server-Side MongoDB Functions Less Awkward

I’ve recently switched my project at work to use MongoDB for the user database and a few other datasets.

Currently I don’t use many JavaScript functions, but when I do I like to store them on the server so that they’re accessible when I’m poking around in a console.

I use something similar to the following function to load all of my JS functions onto the server when my app starts:

import os
import pymongo
import pkg_resources
 
# Relative to distribution's root
SCRIPT_DIR = os.path.join('model', 'js')
 
def init_js(db):
    '''Initializes server-side javascript functions'''
    scripts = filter(
            lambda f: f.endswith('.js'),
            pkg_resources.resource_listdir(__name__, SCRIPT_DIR)
        )
    for script in scripts:
        # Name the function after the script name
        func_name, _ = script.split('.', 1)
        script_path = os.path.join(SCRIPT_DIR, script)
 
        # Create a pymongo Code object
        # otherwise it will be stored as a string
        code = pymongo.code.Code(
                pkg_resources.resource_string(__name__, script_path))
 
        # Upsert the function
        db.system.js.save({ '_id': func_name, 'value': code, })

However, using server-side functions from Python is awkward at best. Say I have the JavaScript function:

add.js

function(x, y) {
    return x + y;
}

To run that function via PyMongo requires wrapping the function call with placeholder parameters in a Code object and passing in values as a dict:

var1 = 1
var2 = 2
result = db.eval(pymongo.code.Code('add(a, b)', {'a': var1, 'b': var2,}))
assert result == 3

Update: See MongoDB dev Mike Dirolf comment to see a much more concise way of executing server-side functions.

Bearable for simple functions, but having to manually map parameters to values is tiresome and error prone with longer function signatures.

What I wanted was something more natural like:

var1 = 1
var2 = 2
result = db.add(var1, var2)
assert result == 3

I use a simple PyMongo Database object wrapper to make my life easier:

import string
 
from pymongo.code import Code
 
class ServerSideFunctions(object):
    def __init__(self, db):
        self.db = db
 
    def func_wrapper(self, func):
        '''Returns a closure for calling a server-side function.'''
        params = [] # To keep params ordered
        kwargs = {}
        def server_side_func(*args):
            '''Calls server side function with positional arguments.'''
            # Could be removed with better param generating logic
            if len(args) > len(string.letters):
                raise TypeError('%s() takes at most %d arguments (%d given)'
                        % (func, len(string.letters), len(args)))
 
            # Prepare arguments
            for k, v in zip(string.letters, args):
                kwargs[k] = v
                params.append(k) 
 
            # Prepare code object
            code = Code('%s(%s)' % (func, ', '.join(params)), kwargs)
 
            # Return result of server-side function
            return self.db.eval(code)
        return server_side_func
 
    def __getattr__(self, func):
        '''Return a closure for calling server-side function named `func`'''
        return self.func_wrapper(func)
 
dbjs = ServerSideFunctions('foo')
var1 = 1
var2 = 2
result = dbjs.add(var1, var2)
assert result == 3

I’m tempted to monkey-patch PyMongo’s Database class to add a ServerSideFunctions instance directly as a js attribute, so then I could drop the confusing dbjs variable and just use:

assert db.js.add(1,2) == 3

If someone knows of a better way to access server-side MongoDB functions from Python, please let me know!

I modified this code to remove code specific to my project, so please let me know if there are errors.

This entry was posted in Open Source, Python, Technology and tagged , , . Bookmark the permalink.
  • http://www.dirolf.com Mike Dirolf

    One thing that might be a bit nicer would be to define a new anonymous function to call your server-side one. So instead of:

    db.eval(pymongo.code.Code(‘add(a, b)’, {‘a’: var1, ‘b’: var2,}))

    You could do:

    db.eval(“function(a,b) {return add(a,b);}”, var1, var2)

    You won’t need to wrap as code since we don’t need the scope anymore. Still not quite as nice as your helper makes it.

  • http://michael.susens-schurter.com/blog/ Michael Schurter

    Thanks Mike! That’s much better than my initial attempt. Updated the post to point people to your example code.

  • http://tabed.org ph

    you can store your functions in mongodb itself. there’s a spething thing for it,

  • http://www.valentinekidbooks.co.cc/the-west-side-kid-a-novel/ The West Side Kid: A Novel | Children's Books for Valentine's Day :Valentines day kids

    [...] Making Server-Side MongoDB Functions Less Awkward « michael schurter [...]

  • http://michael.susens-schurter.com/blog/ Michael Schurter
  • http://www.dirolf.com Mike Dirolf

    This functionality has been added in 1.4+, accessible through the db.system_js property:

    >>> db.system_js.add = “function(a, b) { return a + b; }”
    >>> db.system_js.add(5, 4)
    9
    >>> del db.system_js.add

  • Mike

    Is there a way to execute a javascript function that’s stored on disk from the Mongodb shell?