Create Reusable Functions with Typescript - javascript

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

Related

Code works fine when value is hardcoded, but fails when value is dynamic

I'm working on a chrome extension that grabs data from Rate My Professor's GraphQL API and then puts that rating on my universities courses portal. Here's my code:
background.js
const {GraphQLClient, gql} = require('graphql-request');
console.log("background.js loaded");
const searchTeacherQuery = gql`
query NewSearchTeachersQuery($text: String!, $schoolID: ID!)
{
newSearch {
teachers(query: {text: $text, schoolID: $schoolID}) {
edges {
cursor
node {
id
firstName
lastName
school {
name
id
}
}
}
}
}
}
`;
const getTeacherQuery = gql`
query TeacherRatingsPageQuery(
$id: ID!
) {
node(id: $id) {
... on Teacher {
id
firstName
lastName
school {
name
id
city
state
}
avgDifficulty
avgRating
department
numRatings
legacyId
wouldTakeAgainPercent
}
id
}
}
`;
const AUTH_TOKEN = 'dGVzdDp0ZXN0';
const client = new GraphQLClient('https://www.ratemyprofessors.com/graphql', {
headers: {
authorization: `Basic ${AUTH_TOKEN}`
}
});
const searchTeacher = async (professorName, schoolID) => {
console.log("searchTeacher called");
console.log(professorName);
console.log(typeof professorName);
console.log(schoolID);
const response = await client.request(searchTeacherQuery, {
text: professorName,
schoolID
});
if (response.newSearch.teachers === null) {
return [];
}
return response.newSearch.teachers.edges.map((edge) => edge.node);
};
const getTeacher = async (id) => {
const response = await client.request(getTeacherQuery, {id});
return response.node;
};
async function getAvgRating(professorName) {
console.log('1: ', professorName);
const teachers = await searchTeacher(professorName, 'U2Nob29sLTE0OTU=');
console.log(teachers);
const teacherID = teachers[0].id;
const teacher = await getTeacher(teacherID);
const avgRating = teacher.avgRating;
console.log(teacher);
console.log(avgRating);
return avgRating;
}
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
console.log('received message from content script:', request);
console.log('test:', request.professorName);
getAvgRating(request.professorName).then(response => {
sendResponse(response);
});
return true;
});
and here's content.js:
const professorLinks = document.querySelectorAll('td[width="15%"] a');
professorLinks.forEach(link => {
const professorName = link.textContent;
console.log(professorName);
chrome.runtime.sendMessage({ professorName }, (response) => {
console.log(response);
if (response.error) {
link.insertAdjacentHTML('afterend', `<div>Error: ${response.error}</div>`);
} else {
link.insertAdjacentHTML('afterend', `<div>${response}/5</div>`);
}
});
});
Now, if I set professorName to a fixed value, like this:
async function getAvgRating(professorName) {
const teachers = await searchTeacher('Hossein Kassiri', 'U2Nob29sLTE0OTU=');
console.log(teachers);
const teacherID = teachers[0].id;
const teacher = await getTeacher(teacherID);
const avgRating = teacher.avgRating;
console.log(teacher);
console.log(avgRating);
return avgRating;
}
the code works as intended, with the expected output:
but if searchTeacher is called with professorName instead of a fixed value like this:
async function getAvgRating(professorName) {
const teachers = await searchTeacher(professorName, 'U2Nob29sLTE0OTU=');
console.log(teachers);
const teacherID = teachers[0].id;
const teacher = await getTeacher(teacherID);
const avgRating = teacher.avgRating;
console.log(teacher);
console.log(avgRating);
return avgRating;
}
it returns an empty object:
dynamic graphql request vs hardcoded graphql request
i'm not sure if i'm missing something trivial, as the values being passed to searchTeacher appear to be exactly the same, but it only works when hardcoded. please let me know if i'm missing something, thank you.
After much troubleshooting, I found the issue. #Damzaky was correct, using the localeCompare method I was able to determine that the string from the HTML and my hardcoded string were not the same, was localeCompare would return -1. I cleansed the string from the HTML using this line of code:
const normalizedName = name.normalize('NFKD');
and passed that to searchTeacher, and now the behaviour is as expected. Thanks to everyone that helped.

Nest JS Factorize Error handling in resolver

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,
};
}
}

How can I avoid instantiating for each method my service and repository

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

passing props as graphql mutation argument in react

My Code looks like this:
interface MutationProps{
username: any,
Mutation: any
}
const UseCustomMutation: React.FC<MutationProps> = (MutationProps: MutationProps) => {
const [myFunc, {data, error}] = useMutation(MutationProps.Mutation);
useEffect(() => {
myFunc({variables:{username: MutationProps.username}})
console.log(JSON.stringify(data))
console.log(JSON.stringify(error, null , 2))
}, [])
return data
}
export const DisplayUser = () => {
const GET_USER = gql`
mutation GetUser($username: String!){
getUser(username: $username) {
pfp
username
password
age
CurrentLive
ismod
description
fullname
}
}
`
const {username} : {username: any} = useParams()
const MyData = UseCustomMutation(username, GET_USER)
console.log(JSON.stringify(MyData))
But I get this error back: ×
Argument of undefined passed to parser was not a valid GraphQL DocumentNode. You may need to use >'graphql-tag' or another method to convert your operation into a document
How about your code looks like this:
interface MutationProps {
username: string;
Mutation: any;
}
const UseCustomMutation: React.FC<MutationProps> = ({ username, Mutation }) => {
const [functionForDoingAction, { data, loading, error }] = useMutation(
Mutation,
{
variables: {
username,
},
}
);
useEffect(() => {
// fn trigger for change data
functionForDoingAction({
variables: {
username: "string_value",
},
});
console.log(JSON.stringify(data));
console.log(JSON.stringify(error, null, 2));
}, []);
if (loading) return "loading...";
if (error) return `Submission error! ${error.message}`;
return data;
};
export const DisplayUser = () => {
const GET_USER = gql`
mutation GetUser($username: String!) {
getUser(username: $username) {
pfp
username
password
age
CurrentLive
ismod
description
fullname
}
}
`;
const { username }: { username: string } = useParams();
const MyData = UseCustomMutation(username, GET_USER);
console.log(JSON.stringify(MyData));
};
you can pass an argument directly to the useMutation hook which they provide as an Options parameter. Or is the direct trigger function from the hook you get.

Why isn't my pushKey being added to my User's post object?

When a user makes a post, I want to take the pushKey of the post and then add it in to the User object in order to store in a list of posts they have made.
I had assumed that my logic was correct but it seems not. Everything is console logging so far.
Here is the Create Post action:
export const createPostNoImage = (text, firstName, university, avatar) => {
const timeDate = new Date().getTime();
const { currentUser } = firebase.auth();
const { uid } = currentUser;
const anonAvatarKey = Math.floor(Math.random() * 10);
return (dispatch) => {
const pushKey = firebase.database().ref('/social/posts/').push().key;
const postObject = {
text,
comment_count: 0,
vote_count: 0,
author: {
uid,
anon_avatar_key: anonAvatarKey,
first_name: firstName,
photo_avatar: avatar,
university
},
created_at: timeDate,
};
firebase.database().ref(`/social/posts/${pushKey}`)
.update(postObject)
.then(() => {
postObject.uid = pushKey;
addPostIdToProfile(pushKey);
dispatch({ type: CREATE_POST, payload: postObject });
});
};
};
The above works just fine, I then call the addPostIdToProfile() function:
const addPostIdToProfile = (pushKey) => {
const { currentUser } = firebase.auth();
console.log(pushKey);
firebase.database().ref(`/social/users/${currentUser.uid}`)
.update((user) => {
if (!user.posts) {
user.posts = {};
}
user.posts[pushKey] = true;
return user;
});
};
It is this that is not updating in the database. Can someone please tell me why?

Categories

Resources