How to Open Realm(JavaScript) object asynchronously and use it with services - javascript

I am using below code in my project to open realm asynchronously and use it with the services.
RmProfile.js:
import Realm from 'realm';
const PrflSchema = {
name: 'Profile',
primaryKey: 'id',
properties: {
id : {type: 'int'},
mob : {type: 'int'},
name : {type: 'string'},
}
};
let RmPrfl;
Realm.open({
schema: [PrflSchema],
schemaVersion: 0
}).then(realm => {
RmPrfl = realm;
})
.catch(error => {
console.log('Error in Opening PrflSchema');
console.log(error);
});
let ProfileServices= {
getName: function(id) {
let PrflInfo=RmPrfl.objectForPrimaryKey('Profile', id);
if(PrflInfo){
return PrflInfo.name;
}
else{
return false;
}
}
}
module.exports = ProfileServices;
Now to use the realm services in other files i am simply trying to export
Profile.js:
import PrflSrvcs from './ProfileServices'
console.log(PrflSrvcs.getName('1253'));
here the services getting exported but the error is coming like RmPrfl is undefined. This happens as the Realm.Open() is asynchronously executed and before its execution ends, the ProfileServices is executed.
So as I am new to Realm, can anyone guide me, How to asynchronous transactions using Realm JavaScript.
Any example will be good to understand.
Thank you..

Why don't you just wrap it all in a promise e.g.
let ProfileServices = () => {
getName: function(id) {
return new Promise((resolve, reject) => {
Realm.open({
schema: [PrflSchema],
schemaVersion: 0
})
.then(realm => {
let PrflInfo = realm.objectForPrimaryKey('Profile', id);
resolve(PrflInfo || false)
})
.catch(error => {
console.log('Error in Opening PrflSchema');
console.log(error);
reject(error)
});
})
}
}
module.exports = ProfileServices
import PrflSrvcs from './ProfileServices'
PrflSrvcs.getName('1253')
.then(name => { console.log(name) });
You probably don't want to be opening the realm for every query though, you can cache it and check for the existence before running the open.

It's not clear in the documentation, but it seems Realm.open(config) caches the promised realm itself, so you can actually just use that API for every query.
# Initial Ream.open(): took 176.50000000139698 milliseconds.
# Manually Cached: took 0.5999999993946403 milliseconds.
# Second Realm.open(): took 0.19999999494757503 milliseconds.
So manually caching just creates a little extra overhead without simplifying the API for accessing the realm.
Here's the caching function I used:
private realm(): Promise<Realm> {
return new Promise((resolve, reject) => {
if (this.cachedRealm) {
resolve(this.cachedRealm)
} else {
Realm.open(this.config)
.then(realm => {
this.cachedRealm = realm
resolve(realm)
})
.catch(error => {
console.error(error);
reject(error)
});
}
})
}

Related

How can I make an Idempotent Callable Function with Firebase Firestore?

Sometimes I'm getting duplicated documents from a callable function that looks like this:
const { default: Big } = require('big.js');
const { firestore } = require('firebase-admin');
const functions = require('firebase-functions');
const { createLog } = require('./utils/createLog');
const { payCart } = require('./utils/payCart');
const { unlockCart } = require('./utils/unlockCart');
exports.completeRechargedTransaction = functions.https.onCall(
async (data, context) => {
try {
if (!context.auth) {
throw new functions.https.HttpsError(
'unauthenticated',
'unauthenticated'
);
}
const requiredProperties = [
'foo',
'bar',
'etc'
];
const isDataValid = requiredProperties.every(prop => {
return Object.keys(data).includes(prop);
});
if (!isDataValid) {
throw new functions.https.HttpsError(
'failed-precondition',
'failed-precondition'
);
}
const transactionRef = firestore()
.collection('transactions')
.doc(data.transactionID);
const userRef = firestore().collection('users').doc(data.paidBy.userID);
let currentTransaction = null;
await firestore().runTransaction(async transaction => {
try {
const transactionSnap = await transaction.get(transactionRef);
if (!transactionSnap.exists) {
throw new functions.https.HttpsError(
'not-found',
'not-found'
);
}
const transactionData = transactionSnap.data();
if (transactionData.status !== 'recharged') {
throw new functions.https.HttpsError(
'invalid-argument',
'invalid-argument'
);
}
if (transactionData.type !== 'recharge') {
throw new functions.https.HttpsError(
'invalid-argument',
'invalid-argument'
);
}
if (transactionData.paidBy === null) {
throw new functions.https.HttpsError(
'invalid-argument',
'invalid-argument',
);
}
const userSnap = await transaction.get(userRef);
if (!userSnap.exists) {
throw new functions.https.HttpsError(
'not-found',
'not-found',
);
}
const userData = userSnap.data();
const newUserPoints = new Big(userData.points).plus(data.points);
if (!data.isGoldUser) {
transaction.update(userRef, {
points: parseFloat(newUserPoints.toFixed(2))
});
}
currentTransaction = {
...data,
remainingBalance: parseFloat(newUserPoints.toFixed(2)),
status: 'completed'
};
transaction.update(transactionRef, currentTransaction);
} catch (error) {
console.error(error);
throw error;
}
});
const { paymentMethod } = data.rechargeDetails;
let cashAmount = 0;
if (paymentMethod && paymentMethod.paymentMethod === 'cash') {
cashAmount = data.points;
}
let cartResponse = null;
if (
data.rechargeDetails.isProcessingCart &&
Boolean(data.paidBy.userID) &&
!data.isGoldUser
) {
cartResponse = await payCart(context, data.paidBy.userID, cashAmount);
// This is the function that does all the writes and for some reason it is getting
// called twice or thrice in some rare cases, and I'm pretty much sure that
// The Angular Client is only calling this function "completeRechargedTransaction " once.
}
await createLog({
message: 'Success',
createdAt: new Date(),
type: 'activity',
collectionName: 'transactions',
callerID: context.auth.uid || null,
docID: transactionRef.id
});
return {
code: 200,
message: 'Success',
transaction: currentTransaction,
cartResponse
};
} catch (error) {
console.error(error);
await unlockCart(data.paidBy.userID);
await createLog({
message: error.message,
createdAt: new Date(),
type: 'error',
collectionName: 'transactions',
callerID: context.auth.uid || null,
docID: data.transactionID,
errorSource:
'completeRechargedTransaction'
});
throw error;
}
}
);
I'm reading a lot of firebase documentation, but I can't find a solution to implement idempotency on my callable functions, the context parameter in callable function is very different from background functions and triggers, the callable context looks like this:
https://firebase.google.com/docs/reference/functions/providers_https_.callablecontext
I did find a helpful blogpost to implement idempotency with firebase triggers:
Cloud Functions pro tips: Building idempotent functions
But I don't fully understand this approach because I think it's assuming that the document writes are made on the client aka the front end application, and I don't really think that's a good approach because is it too reliant on the client and I'm afraid of security issues as well.
So yeah, I would like to know is there's a way to implement Idempotency on Callable Functions, I need something like an EventID but for callable functions to safely implement payments on my app and third party apis, such as stripe.
I will appreciate any help or hint you can give me.
The use of idempotent functions mainly applies to the automatically triggered Cloud Functions that respond to events such as a file uploaded to Cloud Storage or document added to Firestore. In these cases, the event triggers the function to be executed, and if the function succeeds, all is well. However, if the function fails, it will get retried automatically which leads to the problems discussed in the blog post you linked.
In the case of user-triggered cloud functions (a HTTPS Event or Callable cloud function), these are not retried automatically. It is left up to the caller of these functions to choose to handle any errors and whether they are retried by the client calling the function again.
As these user-triggered functions are only executed by your client code, you should check to make sure that completeRechargedTransaction() isn't being called more than once. A method of testing this is to supply your own value for Event ID prior to calling the function like so:
// using a firebase push ID as a UUID
// could also use someFirestoreCollectionReference.doc().id or uuid()
const eventId = firebase.database.ref().push().key;
completeRechargedTransaction({
eventId,
/* ... other data ... */
})
.then(console.log.bind(null, "Successfully completed recharged transaction:"))
.catch(console.error.bind(null, "Failed to complete recharged transaction:"));
Note: One of the most common ways functions will get called twice by the client is because of rerenders where you've updated the state to show a "loading" message and then your call to the function gets made a second time. As an example for React, you would make sure your database call is wrapped in it's own useEffect() call.

Best practise to combine multiple rest calls to populate 1 graphQL type in apollo-server

I have graphql User type that needs information from multiple REST api's and different servers.
Basic example: get the user firstname from rest domain 1 and get lastname from rest domain 2. Both rest domain have a common "userID" attribute.
A simplefied example of my resolver code atm:
user: async (_source, args, { dataSources }) => {
try {
const datasource1 = await dataSources.RESTAPI1.getUser(args.id);
const datasource2 = await dataSources.RESTAPI2.getUser(args.id);
return { ...datasource1, ...datasource2 };
} catch (error) {
console.log("An error occurred.", error);
}
return [];
}
This works fine for this simplefied version, but I have 2 problems with this solution:
first, IRL there is a lot of logic going into merging the 2 json results. Since some field are shared but have different data (or are empty). So it's like cherry picking both results to create a combined result.
My second problem is that this is still a waterfall method. First get the data from restapi1, when thats done call restapi2. Basicly apollo-server is reintroducing rest-waterfall-fetch graphql tries to solve.
Keeping these 2 problems in mind.. Can I optimise this piece of code or rewrite is for better performance or readability? Or are there any packages that might help with this behavior?
Many thanks!
With regard to performance, if the two calls are independent of one another, you can utilize Promise.all to execute them in parallel:
const [dataSource1,dataSource2] = await Promise.all([
dataSources.RESTAPI1.getUser(args.id),
dataSources.RESTAPI2.getUser(args.id),
])
We normally let GraphQL's default resolver logic do the heavy lifting, but if you're finding that you need to "cherry pick" the data from both calls, you can return something like this in your root resolver:
return { dataSource1, dataSource2 }
and then write resolvers for each field:
const resolvers = {
User: {
someField: ({ dataSource1, dataSource2 }) => {
return dataSource1.a || dataSource2.b
},
someOtherField: ({ dataSource1, dataSource2 }) => {
return someCondition ? dataSource1.foo : dataSource2.bar
},
}
}
Assuming your user resolver returns type User forsake...
type User {
id: ID!
datasource1: RandomType
datasource1: RandomType
}
You can create individual resolvers for each field in type User, this can reduce the complexity of the user Query, to only the requested fields.
query {
user {
id
datasource1 {
...
}
}
}
const resolvers = {
Query: {
user: () => {
return { id: "..." };
}
},
User: {
datasource1: () => { ... },
datasource2: () => { ... } // i wont execute
}
};
datasource1 & datasource2 resolvers will only execute in parallel, after Query.user executes.
For parallel call.
const users = async (_source, args, { dataSources }) => {
try {
const promises = [
dataSources.RESTAPI1,
dataSources.RESTAPI2
].map(({ getUser }) => getUser(args.id));
const data = await Promise.all(promises);
return Object.assign({}, ...data);
} catch (error) {
console.log("An error occurred.", error);
}
return [];
};

Optimize Firestore real time updates in chat app

I’m using Firestore real time updates to create realtime chats in my React Native app. I read this may not be the best way to build a chat, but I decided to do so cause I’m using Firebase already and the chat is not the main purpose of the app.
 So, in the context of a real time chat, how would I optimize the Firestore connection? 
It usually works really well but I have experienced a few problems so far:
message comes in slowly
message doesn’t show after being sent
Push notification arrives before the message
These problems usually occur when internet connection is not great (though Whatsapp messages still work fine), but sometimes also on a good connection…
Here is how I query the data (real-time listener added in componentDidMount, removed in componenWillUnmount):
onLogUpdate = (querySnapshot) => {
allMessages = this.state.allMessages
cb = (allMsgs) => {
allMsgs.sort((a,b) => {
a = new Date(a.timestamp);
b = new Date(b.timestamp);
return a>b ? -1 : a<b ? 1 : 0;
})
messages = _.takeRight(allMsgs, this.state.messageLimit)
this.setState({ loading: false, messages, allMessages: allMsgs })
}
async.map(querySnapshot._changes, (change, done) => {
if (change.type === "added") {
const msgData = change._document.data()
if (msgData.origin == 'user') {
this.usersRef.doc(msgData.byWhom).get()
.then(usr => {
msgData.user = usr.data()
done(null, msgData)
})
.catch(err => { console.log('error getting user in log:', err) })
} else {
done(null, msgData)
}
} else {
done(null, 0)
}
}, (err, results) => {
const res = results.filter(el => { return el != 0 })
allMessages = _.concat(allMessages, res)
cb(allMessages)
})
}
And this is how I add new messages:
// in utils.js
exports.addLogMessage = (msgObj, moment_id, callback) => {
logMessagesRef.add(msgObj)
.then(ref => {
momentsRef.doc(moment_id).get()
.then(doc => {
const logMsgs = doc.data().logMessages ? doc.data().logMessages : []
logMsgs.push(ref.id)
momentsRef.doc(moment_id).update({ logMessages: logMsgs })
})
.then(() => {
if (callback) {
callback()
}
})
})
.catch(err => { console.log('error sending logMessage:', err) })
}
// in chat Screen
sendLogMessage = () => {
if (this.state.newMessage.length > 0) {
firebase.analytics().logEvent('send_log_message')
this.setState({ newMessage: '' })
const msgObj = {
text: this.state.newMessage,
origin: 'user',
timestamp: Date.now(),
byWhom: this.state.user._id,
toWhichMoment: this.state.moment._id
}
addLogMessage(msgObj, this.state.moment._id)
}
}
Any suggestions would be highly appreciated :)
I was working on something similar and after i submitted the data it wouldnt show in the ListView. I solved it by pushing the new entry into the preexisting state that was being mapped, and since setState() calls the render() method, it worked just fine. I did something like this:
sendLogMessage().then(newData => {
joined = this.state.data.concat(newData);
this.setState({ data: joined})
})
This is ofcourse assuming that you're using this.state.data.map to render the chat log. This will work when the message that I send cant be seen by me and as for the messages updating as the database updates, you may wanna make use of .onDataChange callback provided by Firebase API. I hope I helped.

ServiceWorker claiming late in the lifecycle to use client.navigate in notificationclick event handler

I have a firebase serviceworker that shows notifications when a message is pushed from Firebase Cloud Messaging (FCM).
It also publishes a post so that my React App can update accordingly.
/* eslint-env worker */
/* eslint no-restricted-globals: 1 */
/* global firebase */
/* global clients */
import config from './config'
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-app.js')
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-messaging.js')
const { FIREBASE_MESSAGING_SENDER_ID } = config
firebase.initializeApp({ messagingSenderId: FIREBASE_MESSAGING_SENDER_ID })
const messaging = firebase.messaging()
messaging.setBackgroundMessageHandler(payload => {
const title = payload.data.title
const options = {
body: payload.data.body,
icon: payload.data.icon,
data: payload.data,
}
clients.matchAll({ includeUncontrolled: true }).then(clientz => {
clientz.forEach(client => {
sendMessageToClient(client, 'NEW_USER_NOTIFICATON')
})
})
return self.registration.showNotification(title, options)
})
const sendMessageToClient = (client, message) => {
const messageChannel = new MessageChannel()
client.postMessage(message, [messageChannel.port2])
}
This all works fine, but I have added it for context.
What I want to do is have a click function that focuses on the correct window/tab and navigates to a link that is passed to it. Or if the tab is not open, open a new window and go to the link.
This is the code I have so far, added to the above file.
self.addEventListener('notificationclick', event => {
const clickedNotification = event.notification
const link = clickedNotification.data.link
clickedNotification.close()
const promiseChain = self.clients.claim()
.then(() => self.clients
.matchAll({
type: 'window',
})
)
.then(windowClients => {
let matchingClient = null
windowClients.forEach(client => {
if (client.url.includes(matching_url)) {
matchingClient = client
}
})
if (matchingClient) {
return matchingClient.navigate(link)
.then(() => matchingClient.focus())
}
return clients.openWindow(link)
})
event.waitUntil(promiseChain)
})
So, I realise that the chained navigate and focus inside a then is probably bad practice, but for now, I am just trying to get it to work. Then I will try and come up with a clever solution.
So the problem with my code is that the clients.claim() doesn't seem to be working. The matchAll doesn't return anything to the next then, the argument is an empty array.
I could simply add the includeUncontrolled: true option to the matchAll, but the navigate command only works on a controlled client instance.
If I try the often referenced Google example for claiming and navigation, it works fine:
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim().then(() => {
// See https://developer.mozilla.org/en-US/docs/Web/API/Clients/matchAll
return self.clients.matchAll({type: 'window'});
}).then(clients => {
return clients.map(client => {
// Check to make sure WindowClient.navigate() is supported.
if ('navigate' in client) {
return client.navigate('activated.html');
}
});
}));
});
So I am stuck.
The serviceworker is activated immediately, so I assume that it claim a client at any point after that.
Have I fallen for a random ServiceWorker Gotcha?
Can the claim only be used and navigated to on the handling of an activation event?
I would appreciate any help available.
Cheers
I couldn't get this to work.
But I thought it would be worth documenting my workaround.
I could not get client.navigate to work in the notificationclick event handler.
So instead I just sent a postMessage containing the URL to be picked up in my app to trigger the redirect there, without any client claiming anywhere.
self.addEventListener('notificationclick', event => {
const clickedNotification = event.notification
const link = clickedNotification.data.link
clickedNotification.close()
const promiseChain = self.clients.matchAll({
type: 'window',
includeUncontrolled: true,
})
.then(windowClients => {
let matchingClient = null
windowClients.forEach(client => {
if (client.url.includes(matching_url)) {
matchingClient = client
}
})
if (matchingClient) {
sendMessageToClient(matchingClient, { type: 'USER_NOTIFICATION_CLICKED', link })
return matchingClient.focus()
}
return clients.openWindow(link)
})
event.waitUntil(promiseChain)
})
const sendMessageToClient = (client, message) => {
const messageChannel = new MessageChannel()
client.postMessage(message, [messageChannel.port2])
}

Support for `{where: 'raw query'}` has been removed

I'm running a GraphQL server using the serverless framework on AWS Lambda.
I'm fetching the data in the UI using apollo-link-batch-http.
If I run it locally using serverless-offline, it works fine. But if I run it on AWS Lambda, it successfully resolves the fooResolver but not the barResolver as it throws the above error message.
The Model.cached(300) is a tiny cache wrapper I made. You can see it here:
https://gist.github.com/lookapanda/4676083186849bb6c5ae6f6230ad7d8f
It basically just makes me able to use my own findById function and so on.
The weird thing is, this error only appears, if I use apollo-link-batch-http but not if I use apollo-link-http. So if the request is batched into a single GraphQL request, there is no such errors (although, then I get this error: https://github.com/sequelize/sequelize/issues/9242)
I really don't know what is going on there, there is no raw where query in any of those resolvers. And it gets even weirder: It only happens with the cached result. The first request is totally valid and successful, but then every consecutive request fails with the above error message.
I really hope someone can help me, I'm getting insane :D
export const fooResolver = async () => {
const Model = db.getDB().sequelize.models.fooModel;
const data = await Model.cached(300).findAll({
where: {
time: {
[Op.gt]: Model.sequelize.literal('CURRENT_TIMESTAMP()'),
},
enabled: true,
state: 'PLANNED',
},
order: [['time', 'DESC']],
limit: 5,
});
return data.value;
};
export const barResolver = async () => {
const models = db.getDB().sequelize.models;
const Model = models.fooModel;
const data = await Model.findById(data.id, {
include: [
{
model: models.barModel,
include: [
{
association: 'fooAssociation',
include: [{ association: 'barAssociation' }],
order: ['showOrder', 'ASC'],
},
],
},
],
});
return {
data,
};
};
I faced similar situation, except in my case using the code below works well:
.findAll({
where: {
title: req.params.title
}
})
Okay, so after tedious debugging I found out that in the cacheable wrapper I was using this snippet:
https://github.com/sequelize/sequelize/issues/2325#issuecomment-366060303
I don't really know still, why exactly this error only showed up on Lambda and not locally, but it stopped erroring when I only used the selectQuery() method and only returned that instead of the whole Model.addHook stuff and so on. So basically changed this
export const getSqlFromSelect = (Model, method, args) => {
if (!SUPPORTED_SELECT_METHODS.includes(method)) {
throw new Error('Unsupported method.');
}
const id = generateRandomHash(10);
return new Promise((resolve, reject) => {
Model.addHook('beforeFindAfterOptions', id, options, => {
Model.removeHook('beforeFindAfterOptions', id);
resolve(
Model.sequelize.dialect.QueryGenerator.selectQuery(
Model.getTableName(),
options,
Model
).slice(0, -1)
);
});
return Model[method](...args).catch(reject);
});
};
to this
export const getSqlFromSelect = (Model, identifier, options) => {
if (typeof identifier === 'number' || typeof identifier === 'string' || Buffer.isBuffer(identifier) {
options.where = {
[Model.primaryKeyAttribute]: identifier,
};
};
return Model.sequelize.dialect.QueryGenerator.selectQuery(
Model.getTableName(),
options,
Model
).slice(0, -1);
};

Categories

Resources