I have a resolver file for my User with some mutations on it to update, delete, markInactive and banUser
async updateUser(
#Args() { id, input },
) {
const user = await this.userService.getById(id);
if (!user) {
return new NotFoundError('User not found');
}
const isAdminUser = this.userService.isUserAdmin(id);
if (!isAdminUser) {
return new PermissionError(`You can't update this user cause it's an admin `);
}
const user = await this.userService.update(id, input);
return {
id: user.id,
user
};
}
async deleteUser(
#Args() { id, input },
) {
const user = await this.userService.getById(id);
if (!user) {
return new NotFoundError('User not found');
}
const isAdminUser = this.userService.isUserAdmin(id);
if (!isAdminUser) {
return new PermissionError(`You can't update this user cause it's an admin`);
}
const user = await this.userService.delete(id, input);
return {
id: user.id,
user
};
}
async deleteUser(
#Args() { id, input },
) {
const user = await this.userService.getById(id);
if (!user) {
return new NotFoundError('User not found');
}
const isAdminUser = this.userService.isUserAdmin(id);
if (!isAdminUser) {
return new PermissionError(`You can't update this user cause it's an admin`);
}
const user = await this.userService.delete(id, input);
return {
id: user.id,
user
};
}
async markInactive(
#Args() { id },
) {
const user = await this.userService.getById(id);
if (!user) {
return new NotFoundError('User not found');
}
const isAdminUser = this.userService.isUserAdmin(id);
if (!isAdminUser) {
return new PermissionError(`You can't update this user cause it's an admin`);
}
const user = await this.userService.markInactive(id);
return {
id: user.id,
user
};
}
async banUser(
#Args() { id },
) {
const user = await this.userService.getById(id);
if (!user) {
return new NotFoundError('User not found');
}
const isAdminUser = this.userService.isUserAdmin(id);
if (!isAdminUser) {
return new PermissionError(`You can't update this user cause it's an admin`);
}
const user = await this.userService.banUser(id);
return {
id: user.id,
user
};
}
I always repeat the error handler (check user exist and check the user is admin) in all my resolver and now I need to add two update mutation, but I want to find a way to factorize this error checking in a common function
Do you have a solution to achieve this ?
great usecase here for refactoring. I would suggest you two approaches. Note that some parts are only guesses as I don't know your code base.
Using a repetitve approach
Since these methods are doing the same thing, you can move the logic somewhere else :
#Injectable()
export class UserService {
async getById(userId: string): Promise<User> {
// just an example
const user = { id: '1', isAdmin: false };
return Promise.resolve(user);
}
async ensuresUserExists(userId: string): Promise<void> {
const user = await this.getById(userId);
if (!user) {
throw new NotFoundError('User not found');
}
}
async ensuresUserIsNotAdmin(userId: string): Promise<void> {
const user = await this.getById(userId);
if (!user) {
throw new PermissionError("You can't update this user cause it's an admin");
}
}
}
You can simply use this in your controller methods:
async updateUser(
#Args() { id, input },
) {
await this.userService.ensuresUserExists(id);
await this.userService.ensuresUserIsNotAdmin(id);
const user = await this.userService.update(id, input);
return {
id: user.id,
user
};
}
I think most ORMs will throw an error if user does not exist anyway, I assume you can go deeper and make sures the user exists and is not admin in the same method but again I don't know what is your architecture.
Using guards
NestJs allows you to create custom guards where you can perform any operations prior to a method execution. It's IMO a cleaner approach.
guard.ts
export const ADMIN_OP = 'admin';
#Injectable()
export class UserEditGuard implements CanActivate {
constructor(private reflector: Reflector, private userService: UserService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const operationType = this.reflector.get<string>(
'operationType',
context.getHandler(),
);
if (operationType !== ADMIN_OP) return true;
const { userId } = request.body; // assuming you are sending params in POST request body
if (!userId) {
throw new UnauthorizedException();
}
await this.userService.ensuresUserExists(userId);
await this.userService.ensuresUserIsNotAdmin(userId);
return true;
}
}
Now you just need to "plug" the guard to your method.
export const UserPermissionCheck = () => SetMetadata('operationType', ADMIN_OP); // This will add the type to metadata
And then add it to your Controller (this will work with a service too)
#Controller()
#UseGuards(UserEditGuard)
export class UserController {
constructor(private readonly userService: UserService) {}
#Post()
#UserPermissionCheck()
async updateUser(#Body() { id, input }) {
const user = await this.userService.update(id, input);
return {
id: user.id,
user,
};
}
#Post()
#UserPermissionCheck()
async deleteUser(#Body() { id, input }) {
const user = await this.userService.delete(id, input);
return {
id: user.id,
user,
};
}
}
Related
Where am i going wrong here?
Using mocha, chai, sinon and proxyquire for an express server and sequelize ORM linked with a postgres database
I am trying to test a login controller route from my express server
Before I show the file which I want to run my test on here is what "../services/authService.js" file looks like
../services/authService
const UserService = require("./userService");
module.exports = class AuthService extends UserService {
};
// so UserService will have the method findByEmail
// UserService class looks like this and it is coming from another file require("./userService.js) as stated above
/*
class UserService {
async findByEmail(email) {
try {
const user = await User.findOne({ where: { email: email }});
if (user) {
return user;
}
throw new Error("User not found");
} catch (err) {
err.code = 404;
throw err
}
}
}
*/
And here is the auth-controller.js file which I want to run the test on
auth-controller.js
const bcrypt = require('bcryptjs');
const AuthService = require("../services/authService"); // is a class which extends from another calls see the code above
const authService = new AuthService();
const jwtGenerator = require('../utils/jwtGenerator');
const createError = require("http-errors");
exports.loginRoute = async (req, res, next) => {
try {
req.body.password = String(req.body.password);
// db query trying to force a sinon.stub to resolve a fake value. But code wont pass here hence 500 error
const userQuery = await authService.findByEmail(req.body.email);
const compare = await bcrypt.compare(req.body.password, userQuery.password);
if (!compare) throw createError(401, 'Incorrect password.');
const user = {
id: userQuery.id, role: userQuery.is_admin ? "Administrator" : "User", email: userQuery.email, Authorized: true
}
const token = jwtGenerator(user);
return res
.cookie("access_token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
}).status(200).json({ message: "Logged in successfully 😊 👌", user, token });
} catch (error) {
next(error);
}
}
This code works in production but I cannot seem to test it. I used proxyquire to require the modules that the function uses. I have a big problem in making proxyquire work when it comes to my class AuthService here is my test file. As proxyquire is not working with classes some how. proxyquire is not using make AuthServiceMock at all cant figure out why.
First of these are my helper variables which I will use in the test file
../test-utils/user-helper
const createAccessToken = (payload) => jwt.sign(payload, TOKEN, {expiresIn: "1h"});
let loginDetail = {
email: "admin#test.com",
password: "123456"
};
let loginAdminUser = {
id: 1,
email: "admin#test.com",
password: "123456",
is_admin: true
}
const loginUser = {
id: 1,
email: "admin#test.com",
password: "123456",
is_admin: true
}
const adminUser = {
id: 1,
email: 'admin#test.com',
password: '123456',
is_admin: true,
first_name: 'john',
last_name: 'doe',
created_at: "2020-06-26T09:31:36.630Z",
updated_at: "2020-06-26T09:31:49.627Z"
}
module.exports = {
createAccessToken,
loginDetail,
loginAdminUser,
loginUser,
adminUser
}
And here is the test file I placed comments espcially around proxyquire when I am trying to use it as this is giving me some issues when it comes to using it with classes. And as well it is not calling mocked/stubbed npm modules for some reason
auth-controller.spec.js
"use strict";
const _ = require("lodash");
const path = require("path");
const proxyquire = require("proxyquire").noCallThru().noPreserveCache();
const chai = require("chai");
const { expect } = chai;
const sinon = require("sinon");
const sinonChai = require("sinon-chai");
chai.use(sinonChai);
// const AuthServiceOriginalClass = require("../../services/authService"); If i use this directly in proxyquire it calls the original class
const { createAccessToken, loginDetail, loginAdminUser, loginUser, adminUser } = require("../test-utils/user-helper");
const controllerPath = path.resolve('./controllers/authController.js');
describe("login route", () => {
let proxy, authService, bcryptStub, fakeCallback, fakeReq, fakeRes, fakeNext, resolveFn, token;
let result, bcryptStubbing, response;
class UserServiceMock {
async findByEmail(email) {
try {
if (email) {
return loginAdminUser;
}
} catch (error) {
throw error;
}
}
}
class AuthServiceMock extends UserServiceMock {};
bcryptStub = {
compare: function() { return true }
};
let tokeen = (kk) => {
return createAccessToken(kk);
}
// token = sinon.mock(createAccessToken(loginAdminUser)); // ?? which 1 to use?
token = sinon.spy(createAccessToken); // ?? which 1 to use?
// token = sinon.stub(createAccessToken) ?? which 1 to use?
proxy = proxyquire(controllerPath, {
"../services/authService.js": AuthServiceMock, // seems like this is not called at all
// "../services/authService.js": AuthServiceOriginalClass, // commented out if use this instead it calls the original class instant
"bcryptjs": bcryptStub,
"../utils/jwtGenerator": token,
// "#noCallThru": true // keep on or off?
});
before("Stub my methods", () => {
authService = new AuthServiceMock();
// If I call the entire loginRoute I want this stub authTry to be called inside of it and resolve that object value
authTry = sinon.stub(authService, "findByEmail").withArgs(loginDetail.email).resolves(loginAdminUser);
sinon.stub(bcryptStub, "compare").resolves(true); // force it to return true as that seems to be like the code of authController.js
// sinon.stub(token, "createAccessToken")
});
before("call the function loginRoute", async () => {
// fakeCallback = new Promise((res, rej) => {
// resolveFn = res
// });
fakeReq = {
body: {
email: loginDetail.email,
password: loginDetail.password
}
};
fakeRes = {
cookie: sinon.spy(),
status: sinon.spy(),
json: sinon.spy()
}
fakeNext = sinon.stub();
await proxy.loginRoute(fakeReq, fakeReq, fakeNext).then((_result) => {
result = _result;
});
console.log("result")
console.log(result) // undefined
console.log("result")
});
it("login route test if the stubs are called", async () => {
expect(authService.findByEmail).to.have.been.called // never called
// expect(bcryptStubbing).to.have.been.called // never called
// expect(response.status).to.deep.equal(200); // doesn't work
}).timeout(10000);
after(() => {
sinon.reset()
});
});
Where am i going wrong here in the test?
I'm learning JavaScript with Express.js Framework. I'm trying to create a simple project for a restaurant (just for learning purposes), and I'm trying to create the CRUD of ingredients. I created the repository for all Prisma (ORM) requests, the service with business logic, and a controller for request/response handling. I wish to know if there is a better way of instantiating the service and repository on my controller. I'm doing that for each method. It worked, but I'm repeating this code block a lot.
This is IngredientRepository:
const prisma = require("../../prisma/client");
class IngredientRepository {
async create({ name, price, image }) {
return await prisma.ingredient.create({
data: {
name,
price,
image,
},
});
}
async findByName({ name }) {
return await prisma.ingredient.findUnique({ where: { name } });
}
async findById({ id }) {
return await prisma.ingredient.findUnique({
where: { id },
});
}
async updateImage({ id, image }) {
return await prisma.ingredient.update({
where: { id },
data: {
image,
},
});
}
}
module.exports = IngredientRepository;
IngredientService:
const Error = require("../middlewares/Error");
const DiskStorage = require("../providers/DiskStorage");
class IngredientService {
constructor(ingredientRepository, userRepository) {
this.ingredientRepository = ingredientRepository;
this.userRepository = userRepository;
}
async create({ name, price, image, loggedUser }) {
const userIsAdmin = await this.userRepository.findById({ loggedUser });
if (userIsAdmin.admin) {
throw Error("", 401);
}
const ingredient = await this.ingredientRepository.findByName({
name: name.toLowerCase(),
});
if (ingredient) {
throw new Error("Ingredient already exists");
}
return await this.ingredientRepository.create({
name: name.toLowerCase(),
price,
image,
});
}
async updateImage({ id, image }) {
const diskStorage = new DiskStorage();
const ingredient = await this.ingredientRepository.findById({ id: id.id });
if (!ingredient) {
throw new Error("This ingredient doesn't exist", 401);
}
if (ingredient.image) {
await diskStorage.deleteFile(ingredient.image);
}
const filename = await diskStorage.saveFile(image);
ingredient.image = filename;
const updatedIngredient = await this.ingredientRepository.updateImage({
id: id.id,
image: ingredient.image,
});
return updatedIngredient;
}
}
module.exports = IngredientService;
IngredientController:
const IngredientRepository = require("../repositories/IngredientRepository");
const UserRepository = require("../repositories/UserRepository");
const IngredientService = require("../services/IngredientService");
class IngredientController {
async create(request, response) {
const ingredientRepository = new IngredientRepository();
const userRepository = new UserRepository();
const ingredientService = new IngredientService(
ingredientRepository,
userRepository
);
const loggedUser = request.user.id;
const { name, price, image } = request.body;
const ingredientCreated = await ingredientService.create({
loggedUser,
name,
price,
image,
});
return response.json(ingredientCreated);
}
async updateImage(request, response) {
const ingredientRepository = new IngredientRepository();
const userRepository = new UserRepository();
const ingredientService = new IngredientService(
ingredientRepository,
userRepository
);
const loggedUser = request.user.id;
const id = request.params;
const image = request.file.filename;
const ingredientWithImageUpdated = await ingredientService.updateImage({
loggedUser,
id,
image,
});
return response.json(ingredientWithImageUpdated);
}
}
module.exports = IngredientController;
It is possible to create your dependencies in constructor of IngredientController and then use that objects in methods.
Let me show an example:
class IngredientRepository {
findByName() {
return 'findByName: ' + Date.now();
}
}
and:
class IngredientController {
constructor (){
this.ingredientRepository = new IngredientRepository();
}
create(){
// you can use your object here
console.log(this.ingredientRepository.findByName())
}
update(){
// you can use your object here
console.log(this.ingredientRepository.findByName())
}
}
An example:
class IngredientRepository {
findByName() {
return 'findByName: ' + Date.now();
}
}
class IngredientController {
constructor (){
this.ingredientRepository = new IngredientRepository();
}
create(){
console.log(this.ingredientRepository.findByName())
}
update(){
console.log(this.ingredientRepository.findByName())
}
}
const ingredientController = new IngredientController();
console.log(ingredientController.create())
console.log(ingredientController.update())
I have a function that returns other functions like so:
export const makeAudienceDb = () => {
async function insert({ ...params }: AudienceAttributes) {
const audience = await AudienceModel.create({ ...params })
const audienceToJson = audience.toJSON()
return audienceToJson
}
async function findById({ id }: { id: number }) {
const user = await AudienceModel.findByPk(id)
return user?.toJSON()
}
async function remove({ id }: { id: number }) {
return AudienceModel.destroy({ where: { id } })
}
async function update({
id,
...changes
}: { id: number } & AudienceAttributes) {
const updated = await AudienceModel.update(
{ ...changes },
{ where: { id } }
)
return updated
}
return Object.freeze({
insert,
findById,
remove,
update,
})
}
I have other models, e.g UserModel, PostModel which have the same database operations as makeAudienceDb. e.g
export const makeUsersDb = ({ hashPassword, createToken }: DBDeps) => {
async function insert({ ...params }: User) {
if (params.password) {
params.password = await hashPassword(params.password)
}
const newUser = await UserModel.create({ ...params })
const returnedUser = newUser.toJSON()
const { id } = newUser
const { name, username, email, password } = returnedUser as User
const payload = {
id,
email,
}
const token = createToken(payload)
const user = { id, name, username, password, email }
return { user, token }
}
async function findById({ id }: { id: number }) {
const user = await UserModel.findByPk(id)
return user?.toJSON()
}
async function findByEmail({ email }: { email: string }) {
const user = await UserModel.findOne({ where: { email } })
return user?.toJSON()
}
async function remove({ id }: { id: number }) {
return UserModel.destroy({ where: { id } })
}
async function update({ id, ...changes }: { id: number } & User) {
const updated = await UserModel.update({ ...changes }, { where: { id } })
return updated
}
return Object.freeze({
insert,
findByEmail,
findById,
remove,
update,
})
}
This leads to code duplication across various levels. I want to know how to create one major function that I can reuse for other database operations. So instead of me having makeAudienceDb, makeUsersDb, I could just have one major function e.g majorDatabaseOps where makeAudienceDb and makeUsersDb could just inherit from. I know this is possible with ES6 Classes and Interfaces but I was just wondering how I could implement the same in a functional way. Any contribution is welcome. Thank you very much!
It looks like what you are trying to do is bind shortcuts to particular sequelize methods. The shared functionality can be implemented using typescript generics. Overriding specific behaviors, like hashing a password for a User makes this a bit more complex.
My first instinct is to use a class-based approach. But you can do it with functions by copying all of the methods from the base and then overriding or adding specific ones, along these lines:
const userDb = Object.freeze({
...makeDb(UserModel),
findByEmail: async ({email}: {email: string}) => {
}
})
We want to create a function that takes the model as an argument. It will use typescript generics to describe the types associated with that model. The generics will be inferred from the model variable when calling the function.
A sequelize Model has two generic values: TModelAttributes and TCreationAttributes which is optional and defaults to TModelAttributes. We also want to require that all of our model attributes must include {id: number}.
You could potentially add additional typings to get better support for toJSON. The sequelize package just declares the return type from Model.toJSON as object which is vague and unhelpful.
Our general function looks like this:
import {Model, ModelCtor} from "sequelize";
export const makeDb = <TModelAttributes extends {id: number} = any, TCreationAttributes extends {} = TModelAttributes>(
model: ModelCtor<Model<TModelAttributes, TCreationAttributes>>
) => {
async function insert({ ...params }: TCreationAttributes) {
const created = await model.create({ ...params });
return created.toJSON();
}
async function findById({ id }: { id: number }) {
const found = await model.findByPk(id)
return found?.toJSON()
}
async function remove({ id }: { id: number }) {
return model.destroy({ where: { id } })
}
async function update({ ...changes }: { id: number } & Partial<TModelAttributes>) {
const { id } = changes; // I get a TS error when destructuring this in the args
const updated = await model.update(
{ ...changes },
{ where: { id } }
);
return updated;
}
return Object.freeze({
insert,
findById,
remove,
update,
});
}
For audience, you would simply call:
const audienceDb = makeDb(AudienceModel);
Or you could define it as a function if you wanted to:
const makeAudienceDb = () => makeDb(AudienceModel);
For the users database, we need to override insert, add findByEmail, and take additional arguments hashPassword and createToken.
This is not elegant, but it should work. I don't love that your return type for user insert is incompatible with the insert returned value from the general makeDb.
export const makeUsersDb = ({ hashPassword, createToken }: DBDeps) => {
// declaring this up top so that you could call methods on it in your overrides
const db = makeDb(UserModel);
async function insert({ ...params }: User) {
if (params.password) {
params.password = await hashPassword(params.password)
}
const newUser = await UserModel.create({ ...params })
const returnedUser = newUser.toJSON()
const { id } = newUser
const { name, username, email, password } = returnedUser as User
const payload = {
id,
email,
}
const token = createToken(payload)
const user = { id, name, username, password, email }
return { user, token }
}
async function findByEmail({ email }: { email: string }) {
const user = await UserModel.findOne({ where: { email } })
return user?.toJSON()
}
return Object.freeze({
...db,
insert,
findByEmail
})
}
How about defining the common functions outside of the majorDatabaseOps function? Will let you reuse them in different places.
//defined outside for reusability
function findById(model, id) {
return model.findByPk(id)
}
const majorDatabaseOps = model => {
function removeById(model, id) {
return model.remove(id);
}
return Object.freeze({
removeById: id => removeById(model, id),
findById: id => findById(model, id),
})
}
//mocking models for demonstration
const UserModel = {
modelName: "UserModel",
findByPk: function(id) {
return console.log(id + " was found in " + this.modelName)
},
remove: function(id) {
return console.log(id + " was removed from " + this.modelName)
}
}
const PostModel = {
modelName: "PostModel",
findByPk: function(id) {
return console.log(id + " was found in " + this.modelName)
},
remove: function(id) {
return console.log(id + " was removed from " + this.modelName)
}
}
const userDbOps = majorDatabaseOps(UserModel);
userDbOps.findById(1);
userDbOps.removeById(1);
const postDbOps = majorDatabaseOps(PostModel);
postDbOps.findById(2);
postDbOps.removeById(2);
I am trying to create a class that will fetch / cache users from my Firestore database. For some reason, I can't seem to save or expose the previous promise that was created. Here is my class:
export class UserCache {
private cacheTimeMilliseconds: number = 600000;
private userCache: any = {};
public getCacheUser(userid: string): Promise<User> {
return new Promise((resolve, reject) => {
let d = new Date();
d.setTime(d.getTime() - this.cacheTimeMilliseconds);
if (this.userCache[userid] && this.userCache[userid].complete && this.userCache[userid].lastAccess > d.getTime()) {
console.log("User cached");
resolve(this.userCache[userid].user);
}
console.log("Need to cache user");
this.userCache[userid] = {
complete: false
};
this.getSetUserFetchPromise(userid).then((data) => {
let user: User = <User>{ id: data.id, ...data.data() };
this.userCache[userid].user = user;
this.userCache[userid].complete = true;
this.userCache[userid].lastAccess = Date.now();
resolve(user);
});
});
}
private getSetUserFetchPromise(userid: string): Promise<any> {
console.log(this.userCache[userid]);
if (this.userCache[userid] && this.userCache[userid].promise) {
return this.userCache[userid].promise;
} else {
console.log("Creating new user fetch request.");
this.userCache[userid].promise = firestore().collection('users').doc(userid).get();
console.log(this.userCache[userid]);
return this.userCache[userid].promise;
}
}
}
Logs: (there are only 2 unique users, so should only be creating 2 new requests)
In the logs I can see that the promise is getting set in getSetUserFetchPromise, but the next time the function is called, the property is no longer set. I suspect it is either a scope or concurrency issue, but I can't seem to get around it.
I am calling getCacheUser in a consuming class with let oCache = new UserCache() and oCache.getCacheUser('USERID')
Edit following Tuan's answer below
UserCacheProvider.ts
import firestore from '#react-native-firebase/firestore';
import { User } from '../static/models';
class UserCache {
private cacheTimeMilliseconds: number = 600000;
private userCache: any = {};
public getCacheUser(userid: string): Promise<User> {
return new Promise((resolve, reject) => {
let d = new Date();
d.setTime(d.getTime() - this.cacheTimeMilliseconds);
if (this.userCache[userid] && this.userCache[userid].complete && this.userCache[userid].lastAccess > d.getTime()) {
console.log("User cached");
resolve(this.userCache[userid].user);
}
console.log("Need to cache user");
this.userCache[userid] = {
complete: false
};
this.getSetUserFetchPromise(userid).then((data) => {
let user: User = <User>{ id: data.id, ...data.data() };
this.userCache[userid].user = user;
this.userCache[userid].complete = true;
this.userCache[userid].lastAccess = Date.now();
resolve(user);
});
});
}
private getSetUserFetchPromise(userid: string): Promise<any> {
console.log(this.userCache[userid]);
if (this.userCache[userid] && this.userCache[userid].promise) {
return this.userCache[userid].promise;
} else {
console.log("Creating new user fetch request.");
this.userCache[userid].promise = firestore().collection('users').doc(userid).get();
console.log(this.userCache[userid]);
return this.userCache[userid].promise;
}
}
}
const userCache = new UserCache();
export default userCache;
ChatProvider.ts (usage)
let promises = [];
docs.forEach(doc => {
let message: Message = <Message>{ id: doc.id, ...doc.data() };
promises.push(UserCacheProvider.getCacheUser(message.senderid).then((oUser) => {
let conv: GCMessage = {
_id: message.id,
text: message.messagecontent,
createdAt: new Date(message.messagedate),
user: <GCUser>{ _id: oUser.id, avatar: oUser.thumbnail, name: oUser.displayname }
}
if (message.type && message.type == 'info') {
conv.system = true;
}
if (message.messageattachment && message.messageattachment != '') {
conv.image = message.messageattachment;
}
return conv;
}));
});
Promise.all(promises).then((values) => {
resolve(values);
});
Without seeing the calling code, it could be that getCacheUser is called twice before firestore resolves.
As an aside, I think refactoring the class may make debugging easier. I wonder why it caches the user, promise completion status, and the promise itself. Why not just cache the promise, something like:
interface UserCacheRecord {
promise: Promise<User>
lastAccess: number
}
export class UserCache {
private cacheTimeMilliseconds: number = 600000;
private userCache: { [userid: string]: UserCacheRecord } = {};
public async getCacheUser(userid: string): Promise<User> {
let d = new Date();
const cacheExpireTime = d.getTime() - this.cacheTimeMilliseconds
if (this.userCache[userid] && this.userCache[userid].lastAccess > cacheExpireTime) {
console.log("User cached");
return this.userCache[userid].promise
}
console.log("Need to cache user");
this.userCache[userid] = {
promise: this.getUser(userid),
lastAccess: Date.now()
}
return this.userCache[userid].promise
}
private async getUser(userid: string): Promise<User> {
const data = firestore().collection('users').doc(userid).get();
return <User>{ id: data.id, ...data.data() };
}
}
Currently, you create new UserCache everytime you access cache users. You have to export the instance of UserCache class, so just single instance is used for your app.
UserCache.ts
class UserCache {
}
const userCache = new UserCache();
export default userCache;
SomeFile.ts
import UserCache from './UserCache';
UserCache.getCacheUser('USERID')
Update
Added some tests
class UserCache {
userCache = {};
getUser(id) {
return new Promise((resolve, reject) => {
if (this.userCache[id]) {
resolve({
...this.userCache[id],
isCache: true,
});
}
this.requestUser(id).then(data => {
resolve(data);
this.userCache[id] = data;
});
});
}
requestUser(id) {
return Promise.resolve({
id,
});
}
}
const userCache = new UserCache();
export default userCache;
userCache.test.ts
import UserCache from '../test';
describe('Test user cache', () => {
test('User cached successfully', async () => {
const user1: any = await UserCache.getUser('test1');
expect(user1.isCache).toBeUndefined();
const user2: any = await UserCache.getUser('test1');
expect(user2.isCache).toBe(true);
});
});
I cannot use beginDialog with the LUIS dispatch. I want to use processHomeAutomation function to begin a new dialog but it gives me an error.
[onTurnError]: Error: DialogContext.beginDialog(): A dialog with an id
of 'TOP_LEVEL_DIALOG' wasn't found.
I imported TOP_LEVEL_DIALOG but still does not work. It only works with MainDialog which is the name of the current class.
const { ConfirmPrompt, DialogSet, DialogTurnStatus, OAuthPrompt, WaterfallDialog } = require('botbuilder-dialogs');
const { LogoutDialog } = require('./logoutDialog');
const { TopLevelDialog, TOP_LEVEL_DIALOG } = require('./topLevelDialog');
const { LuisRecognizer, QnAMaker } = require('botbuilder-ai');
const CONFIRM_PROMPT = 'ConfirmPrompt';
const MAIN_DIALOG = 'MainDialog';
const MAIN_WATERFALL_DIALOG = 'MainWaterfallDialog';
const OAUTH_PROMPT = 'OAuthPrompt';
let loggedIn = true;
class MainDialog extends LogoutDialog {
constructor() {
super(MAIN_DIALOG, process.env.connectionName);
this.addDialog(new TopLevelDialog());
this.addDialog(new OAuthPrompt(OAUTH_PROMPT, {
connectionName: process.env.connectionName,
text: 'Please Sign In',
title: 'Sign In',
timeout: 300000
}));
this.addDialog(new ConfirmPrompt(CONFIRM_PROMPT));
this.addDialog(new WaterfallDialog(MAIN_WATERFALL_DIALOG, [
this.promptStep.bind(this),
this.loginStep.bind(this),
this.displayTokenPhase1.bind(this),
this.displayTokenPhase2.bind(this)
]));
}
/**
* The run method handles the incoming activity (in the form of a DialogContext) and passes it through the dialog system.
* If no dialog is active, it will start the default dialog.
* #param {*} dialogContext
*/
async run(context, accessor) {
console.log(this.id)
if (loggedIn) {
const dialogSet = new DialogSet(accessor);
dialogSet.add(this);
const dialogContext = await dialogSet.createContext(context);
const results = await dialogContext.continueDialog();
const dispatchRecognizer = new LuisRecognizer({
applicationId: process.env.LuisAppId,
endpointKey: process.env.LuisAPIKey,
endpoint: `https://${process.env.LuisAPIHostName}.api.cognitive.microsoft.com`
}, {
includeAllIntents: true,
includeInstanceData: true
}, true);
const qnaMaker = new QnAMaker({
knowledgeBaseId: process.env.QnAKnowledgebaseId,
endpointKey: process.env.QnAAuthKey,
host: process.env.QnAEndpointHostName
});
this.dispatchRecognizer = dispatchRecognizer;
this.qnaMaker = qnaMaker;
const recognizerResult = await dispatchRecognizer.recognize(context);
// Top intent tell us which cognitive service to use.
const intent = LuisRecognizer.topIntent(recognizerResult);
// Next, we call the dispatcher with the top intent.
await this.dispatchToTopIntentAsync(context, intent, recognizerResult, dialogContext, results, dialogSet);
} else {
const dialogSet = new DialogSet(accessor);
dialogSet.add(this);
const dialogContext = await dialogSet.createContext(context);
const results = await dialogContext.continueDialog();
if (results.status === DialogTurnStatus.empty) {
console.log(this.id)
await dialogContext.beginDialog(this.id);
}
}
}
async promptStep(stepContext) {
return await stepContext.beginDialog(OAUTH_PROMPT);
}
async loginStep(stepContext) {
// Get the token from the previous step. Note that we could also have gotten the
// token directly from the prompt itself. There is an example of this in the next method.
const tokenResponse = stepContext.result;
if (tokenResponse) {
loggedIn = true;
await stepContext.context.sendActivity('You are now logged in.');
return await stepContext.prompt(CONFIRM_PROMPT, 'Would you like to view your token?');
}
await stepContext.context.sendActivity('Login was not successful please try again.');
return await stepContext.endDialog();
}
async displayTokenPhase1(stepContext) {
await stepContext.context.sendActivity('Thank you.');
const result = stepContext.result;
if (result) {
return await stepContext.beginDialog(OAUTH_PROMPT);
}
return await stepContext.endDialog();
}
async displayTokenPhase2(stepContext) {
const tokenResponse = stepContext.result;
if (tokenResponse) {
await stepContext.context.sendActivity(`Here is your token ${tokenResponse.token}`);
}
return await stepContext.endDialog();
}
/// QNA STUFF STATS HERE
async dispatchToTopIntentAsync(context, intent, recognizerResult, dialogContext, results) {
switch (intent) {
case 'automation':
await this.processHomeAutomation(context, recognizerResult.luisResult, dialogContext, results);
break;
case 'qna':
await this.processSampleQnA(context);
break;
default:
this.logger.log(`Dispatch unrecognized intent: ${intent}.`);
await context.sendActivity(`Dispatch unrecognized intent: ${intent}.`);
break;
}
}
async processHomeAutomation(context, luisResult, dialogContext, results, dialogSet) {
return await dialogContext.beginDialog(TOP_LEVEL_DIALOG);
}
async processSampleQnA(context) {
this.logger.log('processSampleQnA');
const results = await this.qnaMaker.getAnswers(context);
if (results.length > 0) {
await context.sendActivity(`${results[0].answer}`);
} else {
await context.sendActivity('Sorry, could not find an answer in the Q and A system.');
}
}
}
module.exports.MainDialog = MainDialog;