Update data without manually property setter in DataStore using AWS Amplify with angular - javascript

I used aws-amplify for a very short time for my project. Recently, I'm facing a serious issue about data updating in DataStore in aws-amplify. After reading the docs from their official site I findout that, If I want to update data in DataStore it worked in Immutable way like this,
For creating data,
await DataStore.save(
new Post({
title: "My First Post",
rating: 10,
status: PostStatus.DRAFT
})
);
and for updating data ,
const original = await DataStore.query(Post, "123");
await DataStore.save(
Post.copyOf(original, updated => {
updated.title = `Any Title`;
})
);
there is no issue with creation but when I goes with update this line is a pain if you have more than 20 property in your object model. Because you have to write all those property and update with it .
updated.title = `Any Title`;
So, I want to update like this without writing all the property in manually,
const updatedTestResult = { ...this.testResult, ...this.testResultForm.value };
var result = await DataStore.save(TestResult.copyOf(this.testResult, updatedTestResult))
now it showing me this error,
TypeError: fn is not a function
at datastore.js:376:17
at produce (immer.esm.js:1:16034)
at Model.copyOf (datastore.js:375:32)
at test-result-form.component.ts:130:41
at Generator.next (<anonymous>)
at asyncGeneratorStep (asyncToGenerator.js:3:1)
at _next (asyncToGenerator.js:25:1)
at asyncToGenerator.js:32:1
at new ZoneAwarePromise (zone.js:1427:29)
at asyncToGenerator.js:21:1
or maybe there have another solution. Can you please me on that ?
TIA

I believe, copyOf function takes 2 parameters, 1st one is object and 2nd is callback. so, u can do something like this
await DataStore.save(
Post.copyOf(original, updated => {
...updated, ...this.testResultForm.value
})
);

Originally this question is answered in Github discussion. For updating a single property at a time, you could do the following:
async function updateTestResult(key, value) {
try {
const testResult = await DataStore.save(
TestResult.copyOf(originalTestResult, (updated) => {
updated[key] = value;
})
);
console.log("TestResult updated:", testResult);
} catch (error) {
console.error("Save failed:", error);
}
}
To update multiple properties at once, you could do the following:
async function updateTestResult(testResultForm) {
try {
const testResult = await DataStore.save(
TestResult.copyOf(originalTestResult, (updated) => {
for (const property in testResultForm) {
updated[property] = testResultForm[property];
}
})
);
console.log("TestResult updated:", testResult);
} catch (error) {
console.error("Save failed:", error);
}
}
if you want to see the original ansewer check out this link : https://github.com/aws-amplify/amplify-js/discussions/9984#discussioncomment-2959980

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.

Can anyone tell me why it is showing array is empty?

I want to send notification to multiple devices and for that am getting the token via querying the docs and saving the token to array but it shows that array is empty. Most probably error is because am not able to add elements in the array.
My code is:-
var registrationTokens=[];
const indexOfSender=usersList.indexOf(chatItem.senderUsername);
let removedUsername=usersList.splice(indexOfSender,1); //to remove the senders name from list
usersList.forEach(async(element)=>{
const query = admin.firestore().collection('users').where("username","==",element);
const querySnapshot = await query.get();
if (querySnapshot.docs.length > 0) {
const doc = querySnapshot.docs[0];
const data = doc.data();
registrationTokens.push(data.androidNotificationToken); //adding token over here
}
else {
console.log("Unable to get token for the username ", element);
}
});
const message =
{
notification: {
title:'Message',
body: body,
imageUrl: url,
},
tokens: registrationTokens,
data: { recipient: senderUserId },
};
admin.messaging().sendMulticast(message)
.then(response =>
{
if (response.failureCount > 0) {
const failedTokens = [];
response.responses.forEach((resp, idx) => {
if (!resp.success) {
failedTokens.push(registrationTokens[idx]);
}
});
console.log('List of tokens that caused failures: ' + failedTokens);
}
else
{
console.log('Successfully sent messages ', response);
}
});
Error
Error: tokens must be a non-empty array
at FirebaseMessagingError.FirebaseError [as constructor] (/workspace/node_modules/firebase-admin/lib/utils/error.js:42:28)
at FirebaseMessagingError.PrefixedFirebaseError [as constructor] (/workspace/node_modules/firebase-admin/lib/utils/error.js:88:28)
at new FirebaseMessagingError (/workspace/node_modules/firebase-admin/lib/utils/error.js:254:16)
at Messaging.sendMulticast (/workspace/node_modules/firebase-admin/lib/messaging/messaging.js:294:19)
at sendNotificationForGroupChat (/workspace/index.js:238:35)
at exports.onCreateMessage.functions.region.firestore.document.onCreate (/workspace/index.js:116:9)
at process._tickCallback (internal/process/next_tick.js:68:7)
async inside forEach does not work the way you expect. If you add some logging, you will see that the loop ends before any of its async work is complete, leaving your tokens array empty before you pass it to FCM. Each iteration through the loop simply generates a promise that is not resolved. You will need to rewrite the code to actually wait for all those promises before calling FCM.
Read more about that:
Using async/await with a forEach loop
for-of loop will work just fine with asynchronous calls :)
Cheers

Example script provided by neo4j for JavaScript won't run

I am very new to the graph database ecosystem and for start I am experimenting with the neo4j. I would very much like to work with node and neo4j. So after a quick search I found neo4j-driver that is an officially supported driver for JavaScript and an example provided which is:
const neo4j = require('neo4j-driver')
const driver = neo4j.driver(uri, neo4j.auth.basic(user, password))
const session = driver.session()
const personName = 'Alice'
try {
const result = await session.run(
'CREATE (a:Person {name: $name}) RETURN a',
{ name: personName }
)
const singleRecord = result.records[0]
const node = singleRecord.get(0)
console.log(node.properties.name)
} finally {
await session.close()
}
// on application exit:
await driver.close()
now when I run this code, I immediately get the following error:
SyntaxError: await is only valid in async function
Now I thought I understood the error that I would have to wrap the try-catch block with anonymous async function to get rid of the error. The changed code body is:
const config = {
"neo4j": {
"url": "neo4j://localhost",
"authUser": "neo4j",
"authKey": "adminPassword"
}
}
const neo4j = require("neo4j-driver");
const driver = neo4j.driver(
config.neo4j.url,
neo4j.auth.basic(config.neo4j.authUser, config.neo4j.authKey)
);
const session = driver.session();
(async () => {
try {
const result = await session.run('CREATE (a:Person {name: $name}) RETURN a', { name: 'Alice' });
const singleRecord = result.records[0];
const node = singleRecord.get(0);
console.log(node.properties.name);
} catch (error) {
console.log("Error Body: ", error);
} finally {
await session.close();
}
})();
await driver.close();
But to my dismay, I have run into another error that is very cryptic:
{ Neo4jError: Could not perform discovery. No routing servers available. Known routing table: RoutingTable[database=Sample database, expirationTime=0, currentTime=1592397056399, routers=[], readers=[], writers=[]]
at captureStacktrace (/Users/pc/node_modules/neo4j-driver/lib/result.js:263:15)
at new Result (/Users/pc/node_modules/neo4j-driver/lib/result.js:68:19)
at Session._run (/Users/pc/node_modules/neo4j-driver/lib/session.js:174:14)
at Session.run (/Users/pc/node_modules/neo4j-driver/lib/session.js:135:19)
at /Users/pc/neoNode.js:20:38
at Object.<anonymous> (/Users/pc/neoNode.js:31:3)
at Module._compile (module.js:653:30)
at Object.Module._extensions..js (module.js:664:10)
at Module.load (module.js:566:32)
at tryModuleLoad (module.js:506:12) code: 'ServiceUnavailable', name: 'Neo4jError' }
I also had some problems with this.
First off, Natam Oliveira is correct. You need to use the bolt protocol, and await promises needs to be within an async function. For some reason the neo4j protocol is used in some examples in the docs. Additionally it would seem both examples currently provided by Neo4j—in the driver-manual and javascript-driver section—causes errors if you use them outside of some kind of unspecified environment.
There were some clues on the npmjs pagckage page, though, so by working them into the existing code, I was at least able to spit out some data. However I'm also wondering on how you could make this work inside the async function, so an explanation to how that could work with this driver would be very welcome.
Here's what worked for me:
const neo4j = require('neo4j-driver')
const cnx = {
user: 'neo4j',
password: 'some passphrase',
uri: 'bolt://localhost:7687'
}
const driver = neo4j.driver(cnx.uri, neo4j.auth.basic(cnx.user, cnx.password))
driver.verifyConnectivity()
.then((cnxMsg) => {
console.log(cnxMsg)
})
const session = driver.session({ database: 'neo4j' })
session.run('MATCH (n:Movie) RETURN n LIMIT 5')
.subscribe({
onKeys: keys => {
console.log(keys)
},
onNext: record => {
console.log(record.get('n').properties.title)
},
onCompleted: () => {
session.close()
},
onError: error => {
console.error(error)
}
})
This spits out some movies using the streaming API as seen in the NPM documentation. (Note: It will only work if you started/installed the Movie database, so double check that you didn't delete it, as its deletion is also part of the Neo4j tutorial.) Now just change the MATCH Cypher query to whatever you like, and play around with the output, for instance by piping it to Express.
Sources:
https://neo4j.com/docs/driver-manual/current/client-applications/
https://neo4j.com/developer/javascript/#javascript-driver
https://www.npmjs.com/package/neo4j-driver
https://neo4j.com/docs/api/javascript-driver/current/
first of all, I think your URL should be "url": "bolt://localhost:7687"
And you still with await driver.close() outside an async function
If you are starting to use neo4j, look for an OGM (Object Graph Model) to help you.

Code in MongoDB triggers file is Producing a Customer.findOne() is not a function error

In order to be able to compare the pre and post-save version of a document, I am trying to lookup the document in a pre hook, and then use that to see what's changed in the doc in the post save hook.
But for some reason I'm getting a "Customer.findOne() is not a function" error. This doesn't make any sense to me because I've imported the model into this triggers file, and then, in my function I do this:
const Customer = require("../customer");
// Get a version of the document prior to changes
exports.preSave = async function(doc) {
console.log("preSave firing with doc._id", doc._id); // this ObjectId logs correctly
if (!doc) return;
this.preSaveDoc = await Customer.findOne({ _id: doc._id }).exec();
console.log("this.preSaveDoc: ", this.preSaveDoc);
};
Again, this code produces an error:
"Customer.findOne() is not a function"
FYI, the relevant code in my Customer model looks like this:
let Schema = mongoose
.Schema(CustomerSchema, {
timestamps: true
})
.pre("count", function(next) {
next();
})
.pre("save", function(next) {
const doc = this;
trigger.preSave(doc);
next();
})
.post("save", function(doc) {
trigger.postSave(doc);
})
.post("update", function(doc) {
trigger.postSave(doc);
})
.post("findOneAndUpdate", function(doc) {
trigger.postSave(doc);
});
module.exports = mongoose.model("Customer", Schema);
What am I missing here? Why would this code produce this error on a very standard MongoDB operation?
This problem has already been solved.
If your mongoDb version is 3.6 or more, you can use change streams
Change streams lets you know what changed in your document. A background process runs in mongoDb which notifies your code an event(CREATE, UPDATE, DELETE) took place and passes you the document. You can filter on your fields to know the exact value.
You can refer this blog
This is a classic use case where change streams can be applied. Better than reinventing the wheel :)
Here is how you can get this to work. Instead of looking up the pre-saved/pre-transformed version of the document via Mongoose like this:
// Get a version of the document prior to changes
exports.preSave = async function(doc) {
console.log("preSave firing with doc._id", doc._id); // this ObjectId logs correctly
if (!doc) return;
this.preSaveDoc = await Customer.findOne({ _id: doc._id }).exec();
console.log("this.preSaveDoc: ", this.preSaveDoc);
};
... look up the document this way:
// Get a version of the document prior to changes
exports.preSave = async function(doc) {
let MongoClient = await require("../../config/database")();
let db = MongoClient.connection.db;
db.collection("customers")
.findOne({ _id: doc._id })
.then(doc => {
this.preSaveDoc = doc;
});
};

Resolving a "TypeError: Cannot read property 'data' of undefined" in Cloud Functions

Sorry if this seems like a really basic question, the concept of cloud functions is extremely new to me and i'm still highly in the learning process.
However, whilst trying to execute this cloud function i get the following error
TypeError: Cannot read property 'data' of undefined
Full log can be seen here
For reference as well, I didnt make this function, im just trying to get it working, i used this video.
The actual cloud function:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
const firestore = admin.firestore();
const settings = { timestampInSnapshots: true };
firestore.settings(settings);
const stripe = require('stripe')(functions.config().stripe.token);
exports.addStripeSource =
functions.firestore.document('cards/{userid}/tokens/{tokenid}')
.onCreate(async (tokenSnap, context) => {
var customer;
const data = tokenSnap.after.data();
if (data === null) {
return null
}
const token = data.tokenId;
const snapchat = await
firestore.collection('cards').doc(context.params.userId).get();
const customerId = snapshot.data().custId;
const customerEmail = snpashot.data().email;
if (customerId === 'new') {
customer = await stripe.customers.create({
email: customerEmail,
source: token
});
firestore.collection('cards').doc(context.params.userId).update({
custId: customer.id
});
}
else {
customer = await stripe.customers.retrieve(customerId)
}
const customerSource = customer.sources.data[0];
return firestore.collection('cards').doc(context.params.userId).collection('sources').doc(customerSource.card.fingerprint).set(customersource, { merge: true });})
The dart code im using for writing a payment service:
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class PaymentService {
addCard(token) {
FirebaseAuth.instance.currentUser().then((user) {
print("Found User");
Firestore.instance
.collection('cards')
.document(user.uid)
.collection('tokens')
.add({'tokenId': token}).then((val) {
print('saved');
});
});
}
}
And finally, what executes when i push the button:
StripeSource.addSource().then((String token) {
print("Stripe!");
PaymentService().addCard(token);
});
As you can see the code is clearly being triggered, but i guess there is some sort of error with the data var, JavaScript is brand new to me so im sure its some sort of very dumb syntax issue.
From the log image attached the error is context is not defined
functions.firestore.document('cards/{userid}/tokens/{tokenid}')
.onCreate(async (tokenSnap, conetxt) => {
In the above function, you have passed parameter as conetxt and later in the function context is used, because of which it is giving undefined error.
Change the parameter name conetxt to context.
As your provided log output explains : you need to define a reference for your firestore.document function :
functions.firestore.document('cards/{userid}/tokens/{tokenid}')
modify it to :
functions.firestore.documentReference(){
}

Categories

Resources