How to pass default parameters to the #Query class in Nest.Js? - node.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)

Related

Avoid multiple Class Instances using Singleton

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.

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))

how to memoize a TypeScript getter

I am using the following approach to memoize a TypeScript getter using a decorator but wanted to know if there is a better way. I am using the popular memoizee package from npm as follows:
import { memoize } from '#app/decorators/memoize'
export class MyComponent {
#memoize()
private static memoizeEyeSrc(clickCount, maxEyeClickCount, botEyesDir) {
return clickCount < maxEyeClickCount ? botEyesDir + '/bot-eye-tiny.png' : botEyesDir + '/bot-eye-black-tiny.png'
}
get leftEyeSrc() {
return MyComponent.memoizeEyeSrc(this.eyes.left.clickCount, this.maxEyeClickCount, this.botEyesDir)
}
}
AND the memoize decorator is:
// decorated method must be pure
import * as memoizee from 'memoizee'
export const memoize = (): MethodDecorator => {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const func = descriptor.value
descriptor.value = memoizee(func)
return descriptor
}
}
Is there a way to do this without using two separate functions in MyComponent and to add the decorator directly to the TypeScript getter instead?
One consideration here is that the decorated function must be pure (in this scenario) but feel free to ignore that if you have an answer that doesn't satisfy this as I have a general interest in how to approach this problem.
The decorator can be extended to support both prototype methods and getters:
export const memoize = (): MethodDecorator => {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
if ('value' in descriptor) {
const func = descriptor.value;
descriptor.value = memoizee(func);
} else if ('get' in descriptor) {
const func = descriptor.get;
descriptor.get = memoizee(func);
}
return descriptor;
}
}
And be used directly on a getter:
#memoize()
get leftEyeSrc() {
...
}
Based on #estus answer, this is what I finally came up with:
#memoize(['this.eyes.left.clickCount'])
get leftEyeSrc() {
return this.eyes.left.clickCount < this.maxEyeClickCount ? this.botEyesDir + '/bot-eye-tiny.png' : this.botEyesDir + '/bot-eye-black-tiny.png'
}
And the memoize decorator is:
// decorated method must be pure when not applied to a getter
import { get } from 'lodash'
import * as memoizee from 'memoizee'
// noinspection JSUnusedGlobalSymbols
const options = {
normalizer(args) {
return args[0]
}
}
const memoizedFuncs = {}
export const memoize = (props: string[] = []): MethodDecorator => {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
props = props.map(prop => prop.replace(/^this\./, ''))
if ('value' in descriptor) {
const valueFunc = descriptor.value
descriptor.value = memoizee(valueFunc)
} else if ('get' in descriptor) {
const getFunc = descriptor.get
// args is used here solely for determining the memoize cache - see the options object
memoizedFuncs[propertyKey] = memoizee((args: string[], that) => {
const func = getFunc.bind(that)
return func()
}, options)
descriptor.get = function() {
const args: string[] = props.map(prop => get(this, prop))
return memoizedFuncs[propertyKey](args, this)
}
}
return descriptor
}
}
This allows for an array of strings to be passed in which determine which properties will be used for the memoize cache (in this case only 1 prop - clickCount - is variable, the other 2 are constant).
The memoizee options state that only the first array arg to memoizee((args: string[], that) => {...}) is to be used for memoization purposes.
Still trying to get my head around how beautiful this code is! Must have been having a good day. Thanks to Yeshua my friend and Saviour :)

Firebase startAfter is not working for doc ref

Docs I am following: https://firebase.google.com/docs/firestore/query-data/query-cursors
I have code like below;
async List(query: any): Promise<Array<any>> {
let collectionQuery = super.GetCollectionReference();
if (query.VideoChannelId) {
collectionQuery = collectionQuery.where(
"VideoChannel.Id",
"==",
query.VideoChannelId
);
}
let startAfterDoc: any = "";
if (query.StartAfter) {
startAfterDoc = await super.GetDocumentReference(query.StartAfter);
}
collectionQuery = collectionQuery
.orderBy(query.OrderBy, "desc")
.startAfter(startAfterDoc)
.limit(query.Limit);
let items = await super.List(collectionQuery);
return items;
}
And utility methods:
GetDocumentReference(id: string): any {
return this.GetCollectionReference().doc(id);
}
async GetDocumentSnapshot(id: string): Promise<any> {
return await this.GetDocumentReference(id).get();
}
GetCollectionReference(): any {
return this.db.collection(this.CollectionName);
}
Regardless what ever value I pass for query.StartAfter it always returns top document in the collection.
I am pretty sure document exists with id query.StartAfter.
If I use GetDocumentSnapshot instead of GetCollectionReference, then I am getting parse error at firebase API.
Indexes has been added for query.OrderBy (CreateDate) and Id fields.
What possibly I would be missing here?

Resources