Avoid multiple Class Instances using Singleton - node.js

Project: Create a dynamodb package using #aws-sdk v3 (client-dynamodb & lib-dynamodb)
Base class
class DynamoDB {
#client: DynamoDBDocumentClient;
constructor() {
const db_client = new DynamoDBClient();
this.#client = DynamoDBDocumentClient.from(db_client);
}
// class methods are get_item, delelte_item etc..
// snip--
}
Class for a table with useful methods
class NoSqlTable extends DynamoDB {
table_name: string;
constructor(table_name: DynamodbTableName) {
super();
this.table_name = table_name;
}
async get_record<K,R>(key: K, params: GetRecordParams = {}) {
const result = await super.get_item({ TableName: this.table_name, Key: key, ...params });
return result.Item as R;
}
// other methods
}
Finally, a set of functions exposed to users of the npm package/project
export const get_user = async(user_id:string) => {
const table = new NosqlTable("user");
const user = await table.get_record({ key: user_id });
return user;
}
export const get_accounts = async(user_id:string) => {
const table = new NosqlTable("account");
const accounts = await table.query_table({ IndexName: "user_id-account_id-index", ...});
return accounts;
}
Target:
Each class NosqlTable and DynamoDB is to be initiated once only.
Note: Once means: 1 if in a server setting (long-running process) or once per request if in serverless mode
Each class should only be initialized if used.
Issue
With the current settings, each function exposed to users like get_user, and get_accounts will initialize the NosqlTable class every time they are called and in turn, calls the DynamoDB class.
For example, in a single request, get_user is called 10 times, and get_accounts is called 5 times,
10 calls to initialize the NosqlTable class with table_name="user",
5 calls to initialize the NosqlTable class with table_name="account"
15 calls to the DynamoDB class.
Solution
Attempt 1
Use a singleton pattern,
let singleton_dynamodb: Dynamodb;
const connect_dynamodb = (params) => {
if(!singleton_dynamodb) singleton_dynamodb = new DynamoDB(params);
return singleton_dynamodb;
}
/* The updated `NosqlTable` class now does not extend DynamoDB class; instead calls the `connect_dynamodb` function
*/
class NoSqlTable {
table_name: string;
this.dynamodb = DynamoDB;
constructor(table_name: DynamodbTableName) {
this.table_name = table_name;
this.dynamodb = connect_dynamodb();
}
async get_record<K,R>(key: K, params: GetRecordParams = {}) {
const result = await this.dynamodb.get_item({ TableName: this.table_name, Key: key, ...params });
return result.Item as R;
}
}
let singleton_nosql_table: NosqlTable;
const connect_nosql_table = (table_name) => {
if(!singleton_nosql_table) singleton_nosql_table = new NosqlTable(table_name);
return singleton_nosql_table;
}
export const get_user = async(user_id:string) => {
const table = connect_nosql_table ("user");
const user = await table.get_record({ key: user_id });
return user;
}
Now, if get_user is called the first time, it will call connect_nosql_table which sets the singleton_nosql_table variable
any subsequent calls to this function will return the singleton_nosql_table setup earlier.
Similarly, any call to the new NosqlTable(table_name) internally calls connect_dynamodb rather than extending Dynamodb class.
Is this the correct way to do it, and what are the best practices to improve this setup?
The ultimate target is to improve performance based on the assumption that initializing 1 class instance is more performant than initializing 100 class instances.
Note: The pattern will be used for other packages/projects, like AWS SQS, AWS SNS, and HTTP Client class per domain.

Related

How to pass default parameters to the #Query class in Nest.Js?

I'm trying to pass the default parameters maxnodes=3 and addstats=false to the controller via the #Query parameter in Nest.Js.
The code works fine, but the default parameters are not used. When I pass on the query parameters the ones that are passed are shown, but if none are passed, the default values (3 and false) are not used.
How to fix that?
context.contructor.ts:
import { CreateContextQuery } from './context.query';
import { CreateContextDto } from './context.dto';
#Post('graph')
public async createGraphForContext(
#Body('context') contextData: CreateContextDto,
#Query()
contextQuery: CreateContextQuery,
) {
const before = Date.now();
const { context } = await this.contextService.createContext(contextData);
const graph = await this.contextService.getGraphOfContext(
context.id,
contextQuery.maxnodes,
contextQuery.addstats,
);
}
context.query.ts:
import { ApiProperty } from '#nestjs/swagger';
export class CreateContextQuery {
#ApiProperty({
description: 'Maximum number of nodes to show on the graph',
})
maxnodes;
#ApiProperty({
description: 'Include graph statistics',
})
addstats;
constructor(maxnodes = 3, addstats = false) {
this.maxnodes = maxnodes;
this.addstats = addstats;
}
}
So basically in your DTO, you can give default values.
export class CreateContextQuery {
#IsOptional()
#Type(() => Number)
#IsNumber()
#Min(0)
maxnodes?: number = 3;
#IsOptional()
#Type(() => Boolean)
#IsBoolean()
addstats?: boolean = false;
constructor(maxnodes = 3, addstats = false) {
this.maxnodes = maxnodes;
this.addstats = addstats;
}
}
// as you can see i am using validation too
And in your controller :
#Post('graph')
#UsePipes(new ValidationPipe({ transform: true }))
// you need to add this for tansformation
public async createGraphForContext(
#Body('context') contextData: CreateContextDto,
#Query()
contextQuery: CreateContextQuery,
) {
const before = Date.now();
const { context } = await this.contextService.createContext(contextData);
const graph = await this.contextService.getGraphOfContext(
context.id,
contextQuery.maxnodes,
contextQuery.addstats,
);
}
PS
Also if you want you can add custom decorators, in your case:
// add this decorator
export const GetContextQuery = createParamDecorator((_data: unknown, ctx: ExecutionContext): CreateContextDto => {
const request = ctx.switchToHttp().getRequest();
const query = request.query;
const maxnodes = parseInt(query.maxnodes) || 3;//default values here in case it fails to parse
const addstats = Boolean(query.addstats) || 0;
return { addstats, addstats };
});
and in your controller, you can call the decorator instead of #Query
just add your decorator #GetContextQuery() context: CreateContextDto, and now you do not need the UsePipes
What you receive in the query param is a plain object. You can achieve what you want putting a pipe in your query param and applying a class transform to instantiate the class.
Read this: https://docs.nestjs.com/pipes#providing-defaults
contextQuery isn't an instance of CreateContextQuery because, without any configuration, Nest won't call new CreateContextQuery any time. This is why you end up using pipes (read this https://docs.nestjs.com/techniques/validation#transform-payload-objects)

Executing nested WaterfallDialogs - nodejs

I'm trying to build a requirement system for order dialogs in our bot, so that we can reuse the main structure for different procedures.
enum DialogIds {
// Necessary Ids
oauthPrompt = "oauthPrompt",
// Requirement dialogs
itemWaterfallDialog = "itemWaterfallDialog",
// Reset Dialogs
summaryWaterfallDialog = "summaryWaterfallDialog",
// All other prompts
unrecognizedItemPrompt = "unrecognizedItemPrompt",
beneficiaryConfirmPrompt = "beneficiaryConfirmPrompt",
askBeneficiaryPrompt = "askBeneficiaryPrompt",
reasonPrompt = "reasonPrompt",
orderConfirm = "orderConfirm"
}
export class OrderDialog extends ComponentDialog {
private responseManager: ResponseManager;
private requirementManager: RequirementManager;
private luisResult: RecognizerResult | undefined = undefined;
// TODO: get userState and ConversationState
constructor(
private service: BotServices,
telemetryClient: BotTelemetryClient
) {
super(OrderDialog.name);
this.initialDialogId = OrderDialog.name;
// Response manager serving OrderResponses.json
this.responseManager = new ResponseManager(["fr-fr"], [OrderResponses]);
const routeWaterfallDialog: ((
sc: WaterfallStepContext
) => Promise<DialogTurnResult>)[] = [
this.route.bind(this)
];
this.telemetryClient = telemetryClient;
this.addDialog(
new WaterfallDialog(this.initialDialogId, routeWaterfallDialog)
);
/**
* Order specific dialogs and requirements
*/
const itemWaterfallDialog: WaterfallDialog = new WaterfallDialog(
DialogIds.itemWaterfallDialog,
[this.itemStep.bind(this), this.itemEndStep.bind(this)]
);
this.addDialog(itemWaterfallDialog);
const reqs = [
new Requirement<string>("claimant", false, undefined),
new Requirement<string>(
"item",
true,
undefined,
itemWaterfallDialog,
DialogIds.itemWaterfallDialog
),
];
// Create requirement manager for this dialog
this.requirementManager = new RequirementManager(reqs);
// Add all the prompt
this.addDialog(new ConfirmPrompt(DialogIds.beneficiaryConfirmPrompt));
this.addDialog(new TextPrompt(DialogIds.unrecognizedItemPrompt));
this.addDialog(new TextPrompt(DialogIds.askBeneficiaryPrompt));
this.addDialog(new TextPrompt(DialogIds.reasonPrompt));
this.addDialog(new ConfirmPrompt(DialogIds.orderConfirm));
}
/**
* We save the token, query graph is necessary and
* execute the next dialog if any, if not we'll
* execute the summary waterfallDialog.
* #param sc context
*/
async route(sc: WaterfallStepContext): Promise<DialogTurnResult> {
this.requirementManager.set("claimant", 'nothing');
let next = this.requirementManager.getNext();
while (next) {
await sc.beginDialog(next.dialogId!);
// Execute summary if there are no elements left
if (!this.requirementManager.getNextBool()) {
await sc.beginDialog(DialogIds.summaryWaterfallDialog);
}
next = this.requirementManager.getNext();
}
return sc.endDialog();
}
/**
* ITEM
* #param sc
*/
async itemStep(sc: WaterfallStepContext): Promise<DialogTurnResult> {
// Couldn't recgonize any item
if (this.luisResult!.entities.length === 0) {
await sc.context.sendActivity(
this.responseManager.getResponse(
OrderResponses.itemNotRecognized
)
);
// prompt user for the item again
return await sc.prompt(
DialogIds.unrecognizedItemPrompt,
this.responseManager.getResponse(OrderResponses.rePromptItem)
);
}
const entities = this.luisResult!.entities as generalLuis["entities"];
if (entities.PhoneItem || entities.ComputerItem) {
const item = entities.PhoneItem
? entities.PhoneItem
: entities.ComputerItem;
if (item) {
this.requirementManager.set("item", item[0][0]);
}
}
return await sc.next();
}
async itemEndStep(sc: WaterfallStepContext): Promise<DialogTurnResult> {
// Save result from itemStep(prompt triggered) if any
if (sc.result) {
await sc.context.sendActivity(
this.responseManager.getResponse(OrderResponses.thanksUser)
);
// retrieve item from result and save it
const item = sc.result as string;
this.requirementManager.set("item", item);
}
return sc.endDialog();
}
}
The line
const result = await sc.beginDialog(next.dialogId!);
Is starting a WaterfallDialog declared in the constructor of the Dialog, and the route method is also inside a general waterfallDialog.
The problem is that, when one of the child dialogs prompts the user, the code doesn't wait for the user response, and because of the way the route works it will call the same dialog again(if a value on an object is not filled it will call the indicated dialog, that's what the requirement manager does).
If saving the return from that line, we can see that the status is "waiting", how could I fix it, or should I create independent dialogs for each requirement, and not just waterfallDialogs?
Thanks.

Cannot call function in the same class

I defined a class called Plan. Here is the code:
class Plan {
async getPlanText(ctx) {
return await this.getPlanDetails(ctx);
}
async getPlanDetails(ctx) {
return ...
}
}
exports.Plan = Plan;
I get:
this.getPlanDetails is not a function
What I did wrong?
I used the Plan class in this way:
const { Plan } = require('./controllers/plan.controller');
let planController = new Plan();
console.log(planController.getPlanText('my context'));
try this. Basically you need to bind the function to a class while passing so it knows where to get the dependencies from. You can read more here : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Function/bind
const planController = new Plan();
const menu = new TelegraphInlineMenu(planController.getPlanText.bind(planController))

passing function to a class in nodejs

I have a function that I need to pass to a class I have defined in nodeJs.
The use case scenario is I want to give the implementer of the class the control of what to do with the data received from createCall function. I don't mind if the method becomes a member function of the class. Any help would be appreciated.
//Function to pass. Defined by the person using the class in their project.
var someFunction = function(data){
console.log(data)
}
//And I have a class i.e. the library.
class A {
constructor(user, handler) {
this.user = user;
this.notificationHandler = handler;
}
createCall(){
var result = new Promise (function(resolve,reject) {
resolve(callApi());
});
//doesn't work. Keeps saying notificationHandler is not a function
result.then(function(resp) {
this.notificationHandler(resp);
}) ;
//I want to pass this resp back to the function I had passed in the
// constructor.
//How do I achieve this.
}
callApi(){ ...somecode... }
}
// The user creates an object of the class like this
var obj = new A("abc#gmail.com", someFunction);
obj.createCall(); // This call should execute the logic inside someFunction after the resp is received.
Arrow functions (if your Node version supports them) are convenient here:
class A {
constructor(user, handler) {
this.user = user;
this.notificationHandler = handler;
}
createCall() {
var result = new Promise(resolve => {
// we're fine here, `this` is the current A instance
resolve(this.callApi());
});
result.then(resp => {
this.notificationHandler(resp);
});
}
callApi() {
// Some code here...
}
}
Inside arrow functions, this refers to the context that defined such functions, in our case the current instance of A. The old school way (ECMA 5) would be:
createCall() {
// save current instance in a variable for further use
// inside callback functions
var self = this;
var result = new Promise(function(resolve) {
// here `this` is completely irrelevant;
// we need to use `self`
resolve(self.callApi());
});
result.then(function(resp) {
self.notificationHandler(resp);
});
}
Check here for details: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#No_separate_this

URL to code in node.js applications

I see they use this kind of code to call restful URLs.
Let's say we have /users/{userId}/tasks to create task for a user.
To call this they create another class instead of calling request directly as shown below:
MyAPP.prototype.users = function (userId) {
return {
tasks: function (taskId) {
return this.usersTasks(userId, taskId);
}
}
}
MyAPP.prototype.usersTasks = function (userId, taskId) {
return {
create: function (task, cb) {
make request POST call
}
}
}
Then we can call this as myapp.users('123').tasks().create(task, cb);
What is this kind of coding called and is there any way to automatically generate the code from the URL structure itself?
That is a way of making classes, but I suggest you look into ES6 classes
Defining a class :
class MyAPP {
//:called when created
constructor(name) {
this.name = name;
console.log("[created] MyAPP :",name);
//(a in memory database stored in MyAPP : for example purpose)
this.DB = {'user000':{'tasks':{'task000':'do pizza'},{'task001':'code some magik'}}}
}
//: Get specific taskID for userID
getTask(userID, taskID) {
console.log("[get task]",taskID,"[from user]",userID)
return (this.DB[userID][taskID])
}
//: Get all tasks for userID
allTasks(userID) {
console.log("[get all tasks from user]",userID)
return (this.DB[userID].tasks)
}
//: Create a taskID with taskContent for userID
newTask(userID, taskID, taskContent) {
this.DB[userID].tasks[taskID] = taskContent
}
}
Creating a MyAPP instance :
var myapp = new MyAPP('Pizza API'); //creates a MyAPP with a name
And then (maybe I got your question wrong) using express you would make a server and listen for requests (GET, POST, PUT, ...)
app.get("/APIv1/:userID/:actionID", function(req, res) {
switch(req.params.actionID){
case 'all':
res.send(myapp.allTasks(req.params.userID));
break
default :
res.send("The "+myapp.name+" doesn't support that (yet)")
break
}
});

Resources