NodeJS Using decorators with a singleton class does not save the state of the class after init

Viewed 176

I have a class MongoQueryResolver which contains a dictionary:

private queries: {[key: string]: (params) => any} = {};

This dictionary holds functions by a key, where each function is a query function to MongoDB.

I have created a decorator MongoQuery which represents a mongo query function:

export function MongoQuery(queryName: string) {
    return function decorator(target, key, func) {
        target.register(queryName, func);
    }
}

This decorator calls MongoQueryResolver#register in order to register the query in the dictionary so I can use it by the query name.

Example of a MongoQuery function I created:

@MongoQuery(QueryType.GET_ALL_ENABLED)
public async getAllEnabled(params) {
    const workersDb = MongoService.getBranch(Branches.WORKERS).db("workers");
    const configCollection = await workersDb.collection('config');

    const criteria: any = {isEnabled: true};

    if (params.proxy) {
        criteria.proxy = params.proxy;
    }

    return await configCollection.find(criteria).toArray();
}

and how I use it outside:

MyRoutes.get('/get-enabled', async (req, res) => {
    const data = await MongoQueryResolver.resolve(QueryType.GET_ALL_ENABLED, {proxy: req.query.proxy});
    res.json(data);
});

The issue

When my application inits, I print the dictionary on set:

set GET_ALL_ENABLED {
  value: [Function: getAllEnabled],
  writable: true,
  enumerable: false,
  configurable: true
}
{
  GET_ALL_ENABLED: {
    value: [Function: getAllEnabled],
    writable: true,
    enumerable: false,
    configurable: true
  }
}

But when I get to use the dictionary, its empty ..

I mark the class as a singleton by export default new MongoQueryResolver()

Why does it happen? seems like it's a new instance somehow?

import MongoQueryResolver from '../../services/mongo/mongo-query-resolver'
1 Answers

After debugging for some time, I figured out the issue.

There is one reason that this happens, I hope that I am right about this.

Before I start, let me show a small simplified example:

The decorator

export function MongoQuery(queryName: string) {
    return function decorator(target: any, key: any, func: any) {
        target.register(queryName, func);
    }
}

The Resolver Singleton

class Resolver {
     map: any;

    constructor() {
        this.map = {};
    } 
    
    register(queryName: any, func: any) {
        this.map[queryName] = func;
    }

    resolve(queryName: string) {
       this.map[queryName]();
    }

    @MongoQuery('myQuery')
    public testQuery() {
        console.log("test!");
    }
}
export default new Resolver();

index.ts

Resolver.resolve("myQuery");

The issue with this example which is very related to my original example, is that when the decorator runs on the class creation, it does not run the constructor, nor initializes class variables (if you do map = {} at the top)

Which means that map will only be set after the decorator runs.

To solve this, I will have to check in the register function if this.map exists, and if not, just set it.

register(queryName: any, func: any) {
    this.map = this.map || {};
    this.map[queryName] = func;
}

Now I will always have the map property set.

Now the only issue remaining is the resolve function, when you save the function in the dictionary, it looks like this:

 {
  value: [Function: testQuery],
  writable: true,
  enumerable: false,
  configurable: true
}

Which is why I get is not a function, so instead of this.map[queryName]() I have to run this.map[queryName].value() to run the function.

Hope this helps anyone !

Related