How to add gates to Objection in NodeJS (AWS Lambda)? - node.js

I have an API developed with Serverless and serverless-offline which is deployed to AWS Lambda. Objection/Knex is used for DB, Firebase for authentication, and Middy to manage the requests.
For the logged-in users, I'm trying to:
Automatically add the company ID on inserting to DB
Exit if a user tries to update or delete an entry that belongs to another company
As of now, this is manually checked. I'm trying to figure out if this can be built into the Model so that it can be eliminated from the main code.
The company ID is available in the event and it's possible to exit the process from the static beforeInsert method. Is it possible to pass data from the event to the model?
Handler:
const baseHandler = async (event) => {
const companyId = event?.requestContext?.authorizer?.claims?.companyId;
await event.models.Client.query().insert({
name: event?.body?.name || "",
companyId // This is the current method. I need to eliminate this
});
await event.models.Client.query().where('id', 'uuid').where('companyId', companyId).update({
name: event?.body?.name || "",
}); // Need to remove the check for companyId from here, and add it to the Model
};
const endpoint = middy(baseHandler)
.use(jsonBodyParser())
.use(validator({ eventSchema }))
.use(httpErrorHandler())
.use(useModels());
Model:
module.exports = class Client extends Model {
async $beforeInsert(queryContext) {
this.companyId = '' // Set the company ID from the logged in user
}
static beforeInsert(args) {
if (!belongsToUser) { // This is what I was trying
args.cancelQuery();
}
}
static get tableName() {
return "clients";
}
};

Related

Multiple REACT API calls vs handling it all in NodeJS

I am currently using AWS DynamoDB for the backend and REACT for the frontend with a NodeJS/AWS Lambda frame.
What I'm trying to do:
Get the information of the teammates in a dataset from a teams table that contains a list of the partition keys of the profiles table, and the name of the team set as the sort key, to get their specific information for that team. I then need to get each of the teammate's universal information that is true across all teams, namely their name and profile picture.
I have 2 options:
Either loop through the list of partition keys in the team dataset that was fetched in REACT, and make a couple of API calls to retrieve their specific information, then a couple more for their universal information.
Or, I was wondering if I can instead then implement the same logic, but instead of the API calls, it will be DynamoDB querying in the Lambda function, then collect them all into a JSON object for it to be fetched by the REACT in one swoop.
Would it be a better idea? Or am I simply doing the REACT logic wrong? I am relatively new to REACT so it's definitely possible and am open to tips.
For reference, here's the code:
useEffect(() => {
let theTeammates : (userProfileResponse | undefined)[] = teammateObjects;
(async () => {
let teammatesPromises : any[] = [];
// teammates = the list of partition keys
for (let i = 0; i < teammates.length; i++) {
if(teammates[i] !== '') {
teammatesPromises.push(getSpecificUser(teammates[i], teamName))
}
}
await Promise.all(teammatesPromises)
.then((resolved) => {
if(resolved) {
theTeammates = resolved.map( (theTeammate) => {
if(theTeammate) {
let user: userProfileResponse = {
userKey: theTeammate.userKey,
teamKey: theTeammate.teamKey,
description: theTeammate.description,
userName: theTeammate.userName,
profilePic: defaultProfile,
isAdmin: theTeammate.isAdmin,
role: theTeammate.role,
}
return user as userProfileResponse;
}
})
}
})
setTeammateObjects(theTeammates as userProfileResponse[]);
}) ();
}, [teamData]);
useEffect(() => {
if(teammateObjects) {
(async () => {
let Teammates : any[] = teammateObjects.filter(x => x !== undefined)
Teammates = Teammates.map( (aTeammate) => {
getProfilePicAndName(aTeammate.userKey).then((profile) => {
if(profile) {
aTeammate.userName = profile.userName;
if(profile.profilePic !== undefined && profile.profilePic !== 'none') {
getProfilePic(profile.profilePic).then((picture) => {
if(picture) {
aTeammate.profilePic = picture
return aTeammate
}
})
} else
return aTeammate
}
})
})
}) ();
}
}, [teammateObjects])
I am currently trying to do it through the react. It works for the most part, but I have noticed that sometimes some of the API calls fail, and some of the teammates don't get fetched successfully and never get displayed to the user until the page is refreshed, which is not acceptable.

Error: addReview is not a function in MERN application

I'm following along with a tutorial to create a movie review app on the MERN stack.
When calling my API using Insomnia, I'm getting the error "ReviewsDAO.addReview is not a function"
reviewsDAO.js:
import mongodb from "mongodb"
const ObjectId = mongodb.ObjectId
let reviews
export default class ReviewsDAO{
static async injectDB(conn){
if(reviews){
return
}
try { reviews = await conn.db(process.env.MOVIEREVIEWS_NS).collection('reviews')}
catch(e){
console.error(`unable to establish connection handle in reviewDAO: ${e}`)
}
}
static async addReview(movieId, user, review, date){
try{
const reviewDoc = {
name: user.name,
user_id: user._id,
date: date,
review: review,
movie_id: ObjectId(movieId)
}
return await reviews.insertOne(reviewDoc)
}
catch(e) {
console.error(`unable to post review ${e}`)
return {error: e}
}
}
reviews.controller.js:
import ReviewsDAO from '../dao/reviewsDAO.js'
export default class ReviewsController{
static async apiPostReview(req,res,next) {
const reviewsDAO = new ReviewsDAO()
try{
const movieId = req.body.movie_id
const review = req.body.review
const userInfo = {
name: req.body.name,
_id: req.body.user_id
}
const date = new Date()
const ReviewResponse = await reviewsDAO.addReview(
movieId,
userInfo,
review,
date
)
res.json({status: "success"})
}
catch(e) {
res.status(500).json({error: e.message})
}
}
The reviews collection is also not being created in MongoDB. But maybe that isn't supposed to happen until we create our first review.
Why isn't my function being called appropriately?
You need to instantiate a ReviewsDAO object in order to call its methods.
const reviewsDAO = new ReviewsDAO()
Then you will be able to access the addReview() method
You are calling the class method without initializing the object.
For example:
class Animal{
function sayhello(){
console.log("Hello")
}
}
// I can call the sayhello method by creating an object.
var dog=new Animal()
dog.sayhello() //prints "Hello"
But in your case, you are calling the method without creating the object.
Animal.sayhello()
// It will create an error
So initialise the object and call the method
const reviewsDAO = new ReviewsDAO()
reviewDAO.addreviews()
The static keyword defines static methods for classes.
Static methods are called directly on the class (Car from the example above) - without creating an instance/object (mycar) of the class
So
const Reviewsdao= new ReviewsDAO()
ReviewsDAO().addreviews()
Call the method with the class name after initialising it.

Cloud Functions for Firebase: Not sending notification to all users

I have created an e-commerce Android app with Firebase and a Flutter app to update the Firebase nodes.
I want to send a notification to all users whenever the admin updates the exchange rate.
Below is the code I wrote to send the update notification to all registered tokens using a Cloud Function but it only sends the notification to the Flutter app instead of both apps.
I don't know what I am doing wrong.
Here is my functions code:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.helloWorld=functions.database.ref('Rates').onUpdate(evt =>{
const payload={
notification:{
title:"Today's Rate",
body :"Click here to see today's gold rate",
badge: '1',
sound: 'default'
}
}
return admin.database().ref('fcm-token').once('value').then(allToken =>{
if(allToken.val()){
console.log('token available');
const token = Object.keys(allToken.val());
console.log(admin.messaging().sendToDevice(token, payload));
return admin.messaging().sendToDevice(token, payload);
}else{
console.log('No Token Available');
throw new Error("Profile doesn't exist")
}
});
});
This is an image of my Realtime Database Structure.
Based on your provided screenshot of your database, the data below /fcm-token in your database has two different formats.
When you added the data manually, you used the format:
{
"fcm-token": {
"someRegistrationToken": {
"token": "someRegistrationToken"
},
"someOtherRegistrationToken": {
"token": "someOtherRegistrationToken"
}
...
},
...
}
Whereas, from your "auto registered" entries, you added the tokens using:
{
"fcm-token": {
"somePushId": {
"fcmToken": "someRegistrationToken"
},
"someOtherPushId": {
"fcmToken": "someOtherRegistrationToken"
},
...
},
...
}
In your Cloud Functions code, you collect all the keys stored under /fcm-token into an array using Object.keys(allToken.val()) which will give you an array containing a mix of push IDs and FCM tokens which is not what you want and is why some devices are missing notifications.
So in short, decide on one format or the other.
Temporary work-around
With your existing mixed structure, you can use the following that will ignore what you use as the key and only extracts the token:
return admin.database().ref('fcm-token').once('value').then(allTokensSnapshot => {
if (allTokensSnapshot.exists()) {
console.log('Tokens available');
const tokenArray = [];
allTokensSnapshot.forEach((tokenSnapshot) => {
let token = tokenSnapshot.hasChild('fcmToken')
? tokenSnapshot.child('fcmToken').val()
: tokenSnapshot.child('token').val();
tokenArray.push(token);
});
return admin.messaging().sendToDevice(tokenArray, payload);
} else {
console.log('No tokens available');
throw new Error('No tokens available');
}
});
Database flattening
Personally, I'd flatten it out so you can use your code as-is, but this would require changing the way you add tokens to the database:
Database structure:
{
"fcm-token": {
"someRegistrationToken": true,
"someOtherRegistrationToken": true,
...
}
}
(You could also use the device owner's user ID instead of true if desired)
Client code:
FirebaseInstanceId.getInstance().getInstanceId()
.addOnSuccessListener(new OnSuccessListener<InstanceIdResult>() {
#Override
public void onSuccess(InstanceIdResult result) {
DatabaseReference allTokensRef = FirebaseDatabase.getInstance().getReference().child("fcm-token");
String token = result.getToken();
Log.e("Test", "FCM Registration Token: " + token);
SharedPreferences preferences = this.getSharedPreferences("com.jk.jkjwellers", MODE_PRIVATE);
if(!(preferences.getBoolean("FCMAdded", false))){
allTokensRef.child(token).setValue(true);
preferences.edit().putBoolean("FCMAdded",true).apply();
}
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Log.e("Test", "Failed to get FCM registration token", e);
}
});
Not sure, but try replacing
databaseReference.push().setValue(new Tokens(token));
with
databaseReference.child(token).setValue(new Tokens(token));

Firestore where 'IN' query on google-cloud firestore

(Using typescript for better readability. Vanilla js is always welcome)
Nodejs App, using these imports:
import { FieldPath, DocumentReference } from '#google-cloud/firestore';
and this function
async getByIds(ids: DocumentReference[]) {
const collection = this.db.client.collection('authors');
const query = await collection.where(FieldPath.documentId(), 'in', ids).get();
return query.docs.map(d => ({ id: d.id, ...d.data() }));
}
returns this very specific error:
The corresponding value for FieldPath.documentId() must be a string or a DocumentReference.
The debugger confirms that ids is in fact a DocumentReference array.
Maybe the #google-cloud/firestore package isn't aligned to the firebase one?
EDIT:
as noted by Doug in it's comment, I forgot to put the code for this.db.client. Here you are:
export class DatabaseProvider {
private _db: Firestore;
get client(): Firestore {
return this._db;
}
constructor() {
this._db = new Firestore({
projectId: ...,
keyFilename: ...
});
}
}
And used as
const db = new DatabaseProvider();
It seems like what you're trying to do is a batch get, which is available via a different method: getAll(). I think you want this:
async getByIds(ids: DocumentReference[]) {
return this.db.client.getAll(...ids);
}

Right way to have Authentication and PubSub in GraphQLServer

I'm trying to use subscriptions on my GraphQL server. The problem I'm facing is that I can't have both a middleware to extract my JWT and a PubSub when initializing the GraphQLServer.
This is what I have:
const server = new GraphQLServer({
schema,
context: ({request}) => extractJWT(request),
});
And it works just fine with this:
#Mutation(returns => User)
public async findUser(
#Ctx() context: IContext,
#PubSub() pubsub: PubSubEngine,
) {
const user = await User.findById(context.tokenData.userId)
pubsub.publish('user', { user })
return user
}
But if I register the PubSub on the GraphQLServer initialization context and change my code to the one below, I can't any longer access what I have in my #Ctx decorators.
const pubSub = new PubSub();
const server = new GraphQLServer({
schema,
context: {
ctx: ({request}) => extractJWT(request),
pubSub
},
});
What is the proper way to initialize my GraphQLServer using both JWT extraction middleware and the PubSub?

Resources