I am trying to get id of an object after set that object. But I am getting type error. TypeError: Cannot read properties of undefined (reading 'val'). How should I do that with firebase 9?
Here is the code that I want to work:
set(push(ref(db, "expenses")), expense)
.then((snapshot) => {
console.log(snapshot.val());
dispatch(
addExpense({
id: snapshot.key,
...expense,
})
);
})
.catch((e) => {
console.log("This failed.", e);
});
Thanks in advance.
Why your code doesn't work
The documentation of set(ref, value) shows that is is defined as:
function set(ref: DatabaseReference, value: unknown): Promise<void>
It returns a Promise<void>, so there's no snapshot being passed to your then.
If the promise resolves (and thus your then callback gets called) that the expense was written to the database on the server as is.
How to fix it
If you want to get the key of the push call, you can capture that outside of the set call already:
const newRef = push(ref(db, "expenses"));
set(newRef, expense)
.then(() => {
dispatch(
addExpense({
id: newRef.key,
...expense,
})
);
})
.catch((e) => {
console.log("This failed.", e);
});
Calling push is a pure client-side operation, which is synchronous, so that doesn't require await or then (which should be used with asynchronous operations).
Further considerations
Note though that now you're only showing the expense locally after it's been written to the server. If that is a requirement for your use-case, then 👍. But when using Firebase it is quite common to:
Use a permanent, onValue listener on expenses to show the latest expenses in the UI.
Write the new expense with a simple call, without a then() listener: set(push(ref(db, "expenses")), expense);
The Firebase SDK will then immediately call the local onValue listener with the new value, with the assumption that the write will succeed.
So your UI will show the local value straight away, giving the user an almost instant response.
In the (more uncommon) case that the server (i.e. your security rules) rejects the write operation, the SDK calls onValue again with the corrected data, so your UI can update the state.
Related
I'm at a loose end here and trying to understand the flow of how angular subscriptions work.
I make a call to an API and in the response I set the data in a behaviourSubject. So I can then subscribe to that data in my application.
Normally I would use async pipes in my templates cause its cleaner and it gets rid of all the subscription data for me.
All methods are apart of the same class method.
my first try.....
exportedData: BehaviourSubject = new BehaviourSubject([]);
exportApiCall(id) {
this.loadingSubject.next(true)
this.api.getReport(id).pipe(
catchError((err, caught) => this.errorHandler.errorHandler(err, caught)),
finalize(() => => this.loadingSubject.next(false))
).subscribe(res => {
this.exportedData.next(res)
})
}
export(collection) {
let x = []
this.exportCollection(collection.id); /// calls api
this.exportedData.subscribe(exportData => {
if(exportData){
x = exportData
}
})
}
console.log(x)//// first time it's empthy, then it's populated with the last click of data
/// in the template
<button (click)="export(data)">Export</button>
My problem is....
There is a list of buttons with different ID's. Each ID goes to the API and gives back certain Data. When I click, the console log firstly gives a blank array. Then there after I get the previous(the one I originally clicked) set of data.
I'm obviously not understanding subscriptions, pipes and behavior Subjects correctly. I understand Im getting a blank array because I'm setting the behaviour subject as a blank array.
my other try
export(collection) {
let x = []
this.exportCollection(collection.id).pip(tap(res => x = res)).subscribe()
console.log(x) //// get blank array
}
exportApiCall(id) {
return this.api.getReport(id).pipe(
catchError((err, caught) => this.errorHandler.errorHandler(err, caught))
)
}
Not sure about the first example - the placement of console.log() and what does the method (that is assigned on button click) do - but for the second example, you're getting an empty array because your observable has a delay and TypeScript doesn't wait for its execution to be completed.
You will most likely see that you will always receive your previous result in your console.log() (after updating response from API).
To get the initial results, you can update to such:
public exportReport(collection): void {
this.exportCollection(collection.id).pipe(take(1)).subscribe(res => {
const x: any = res;
console.log(x);
});
}
This will print your current iteration/values. You also forgot to end listening for subscription (either by unsubscribing or performing operators such as take()). Without ending listening, you might get unexpected results later on or the application could be heavily loaded.
Make sure the following step.
better to add console.log inside your functions and check whether values are coming or not.
Open your chrome browser network tab and see service endpoint is get hitting or not.
check any response coming from endpoints.
if it is still not identifiable then use below one to check whether you are getting a response or not
public exportReport(collection): void {
this.http.get(url+"/"+collection.id).subscribe(res=> {console.log(res)});
}
You would use BehaviourSubject, if there needs to be an initial/default value. If not, you can replace it by a Subject. This is why the initial value is empty array as BehaviourSubject gets called once by default. But if you use subject, it wont get called before the api call and you wont get the initial empty array.
exportedData: BehaviourSubject = new BehaviourSubject([]);
Also, you might not need to subscribe here, instead directly return it and by doing so you could avoid using the above subject.
exportApiCall(id) {
this.loadingSubject.next(true);
return this.api.getReport(id).pipe(
catchError((err, caught) => this.errorHandler.errorHandler(err, caught)),
finalize(() => => this.loadingSubject.next(false))
);
}
Console.log(x) needs to be inside the subscription, as subscribe is asynchronous and we dont knw when it might get complete. And since you need this data, you might want to declare in global score.
export(collection) {
// call api
this.exportApiCall(collection.id).subscribe(exportData => {
if (exportData) {
this.x = exportData; // or maybe this.x.push(exportData) ?
console.log(this.x);
}
});
}
I'm trying to fetch real time data from Cloud Firestore using the below code.
export const getRealTimeData = () =>
db
.collection('posts')
.onSnapshot(
(querySnapshot) => {
const posts: any = [];
querySnapshot.forEach((doc) =>
posts.push(Object.assign({
id: doc.id
}, doc.data()))
);
},
);
};
And, I want to use the resultant array to display the data on UI. When I'm doing this, the resultant array is a function but not the actual array of data.
const posts = getRealTimeData();
Here's what I get when I log posts
function () {
i.kT(), o.al(s);
}
Could anyone please point where I went wrong?
Realtime listeners added with onSnapshot() are not compatible with returning values from function calls. That's because they continue to generate new results over time, and would never really "return" anything once. You should abandon the idea of making a synhronous getter type function in this case - they just don't work for what you're trying to do.
Ideally, you would use an architecture like Redux to manage the updates as they become available. Your realtime listener would dispatch query updates to a store, and your component would subscribe to that store that to receive those updates.
If you don't want to use Redux (which is too bad - you really should for this sort of thing), then you should wrap your query inside a useEffect hook, then have your listener set a state hook variable so your component can receive the updates.
is there a way to perform batched transactions on different fields in realtime database with admin sdk? Currently, I'm using the following:
exports.function = functions.https.onCall((data, context) => {
var transactions = new Object();
transactions[0] = admin.database().ref('ref1/')
.transaction(currentCount => {
return (currentCount || 0) + 1;
}, (error, committed, dataSnapshot) => {...})
transactions[1] = admin.database().ref('ref2/')
.transaction(currentCount => {
return (currentCount || 0) + 1;
}, (error, committed, dataSnapshot) => {...})
return admin.database().ref().update(transactions)
// |^| error occurs right above '|^|', but i don't know why, i suspect it may have something to do with transactions object, and if so, what's the proper way to do batched transactions?
.then(result => {...})
.catch(error => {
console.error('error: ' + error)
})
}
but every time this function is called, although the transactions do work as a batch, the following error is thrown:
Unhandled error TypeError: obj.hasOwnProperty is not a function
at each (/srv/node_modules/#firebase/database/dist/index.node.cjs.js:541:17)
at validateFirebaseData (/srv/node_modules/#firebase/database/dist/index.node.cjs.js:1470:9)
at /srv/node_modules/#firebase/database/dist/index.node.cjs.js:1487:13
at each (/srv/node_modules/#firebase/database/dist/index.node.cjs.js:542:13)
at validateFirebaseData (/srv/node_modules/#firebase/database/dist/index.node.cjs.js:1470:9)
at /srv/node_modules/#firebase/database/dist/index.node.cjs.js:1559:9
at each (/srv/node_modules/#firebase/database/dist/index.node.cjs.js:542:13)
at validateFirebaseMergeDataArg (/srv/node_modules/#firebase/database/dist/index.node.cjs.js:1557:5)
at Reference.update (/srv/node_modules/#firebase/database/dist/index.node.cjs.js:13695:9)
at admin.firestore.collection.doc.collection.doc.create.then.writeResult (/srv/index.js:447:43)
You can't pass a bunch of transactions into a update() call, which is what the error message is (admittedly somewhat confusingly) is trying to tell you.
Firebase has no concept of nested, or batched, transactions. If you need to perform a transaction over multiple locations, you will need to run this as a single transaction call over a node that is above all those locations. As you can probably guess, the contention on such a multi-location transaction is very quickly going to be a throughput limit, so you'll want to consider alternative solutions.
The "simplest" approach I can think of for your use-case, is to replace the two transactions with a single multi-location update, and then use server-side security rules to validate the operation.
For an example of how to do something similar, see my answer here: Is the way the Firebase database quickstart handles counts secure?
With this approach you prevent most of the contention, as the multi-location update doesn't need to read-send-check the entire top-level node, but merely the lower-level nodes that you're updating.
You may have to modify you data structure, and possibly even write additional data, to allow this approach. But in return you'll get a much more scalable transactional update.
Trying to use the admin SDK from Firebase to set custom claims within Firebase Cloud Functions. The issue seems to be the claims object I pass into the function. I understand what a circular object structure is, but I'm not sure why it is happening here.
The Error:
Here is the cloud function code
exports.setCustomClaims2 = functions.https.onCall((uid, claims) => {
return admin.auth().setCustomUserClaims(uid,claims).then(() => {
return {
message: `Success! User updated with claims`
}
})
.catch(err => {
return err;
})
});
And here is the front end code to call it:
let uid = "iNj5qkasMdYt43d1pnoEAIewWWC3";
let claims = {admin: true};
const setCustomClaims = firebase.functions().httpsCallable('setCustomClaims2');
setCustomClaims(uid,claims)
What is interesting is that when I replace the claims parameter directly in the cloud function call like so
admin.auth().setCustomUserClaims(uid,{admin: true})
This seems to work just fine.
Is there a difference in how the object gets received as a parameter?
You're not using the callable type function correctly. As you can see from the documentation, the function you pass to the SDK always receives two arguments, data and context, no matter what you pass from the app. A single object you pass from the app becomes the single data parameter. You can't pass multiple parameters, and that parameter doesn't get broken up into multiple parameters.
What you should do instead is combine uid and claims into a single object, and pass it:
setCustomClaims({ uid, claims })
Then receive it as a single parameter in the function:
exports.setCustomClaims2 = functions.https.onCall((data, context) => {
// data here is the single object you passed from the client
const { uid, claims } = data;
})
I'll note that using console.log in the function will help you debug what your function is doing. If you logged the values of uid and claims, this probably would have been easier to figure out.
I have an angular application that makes a request to an Http service and calls a switchMap on another Http service. For some reason the request in the switchMap only runs the first time the parent call is called. Otherwise the parent request fires and the switchMap one doesn't, here is the code:
this._receivableService.newTenantDebitCredit(tenantCredit)
.take(1)
.switchMap(result =>
// Refresh the lease receivables before giving result
this._receivableService.getAll({
refresh: true,
where: { leaseId: this.leaseId }
}).take(1).map(() => result)
)
.subscribe(
...
)
How can I make the getAll request in the switch map run every time the newTenantDebitCredit method is called above it?
Edit: Here is the entirety of the function that is called on click. when i click the button the first time for a given unit both methods are executed. If I try a Unit that has already had that method called (without a refresh) only the first method is executed. I realize a lot of this may not be clear it's a rather large project at this point.
public submitTenantCredit() {
this.isLoading = true;
let tenantCredit: NewTenantDebitCreditData;
let receivableDefinitions: ReceivableDefinition[] = [];
// construct receivable defintions for NewTenantDebitData model
receivableDefinitions = this._constructReceivableDefinitions();
// construct data we will be POSTing to server.
tenantCredit = new NewTenantDebitCreditData({
siteId: this._apiConfig.siteId,
leaseId: this.leaseId,
isCredit: true,
receivables: receivableDefinitions,
reason: this.actionReason
});
// make service call and handle response
this._receivableService.newTenantDebitCredit(tenantCredit)
.take(1)
.switchMap(result =>
// Refresh the lease receivables before giving result
this._receivableService.getAll({
refresh: true,
where: { leaseId: this.leaseId }
}).take(1).map(() => result)
)
.take(1)
.subscribe(
(receivables) => {
this.closeReasonModal();
let refreshLeaseId = this.leaseId;
this.leaseId = refreshLeaseId;
this.isLoading = false;
this.refreshBool = !this.refreshBool;
this._debitCreditService.refreshUnitInfo();
this._notifications.success(`The tenant credit for ${this.customerName} - Unit ${this.unitNumber} was submitted successfully`);
},
(error) => {
console.error(error);
this.isLoading = false;
}
)
}
If it helps newTenantDebitCredit() is a HTTP POST request and getAll() is a GET request.
You used take operator. When your service observable will emit then take operator will execute first and take will chain only first emit from observable. Subsequent emit will not taken by your code.
If you want to take all emits from observable then remove take from your code.
Hope it will help.
Testing the Rx code in isolation, here's a mockup. The console logs happen each time, so I think the Rx you're using is ok.
The best guess at a likely culprit is this.refreshBool = !this.refreshBool, but we'd need to see the internals of newTenantDebitCredit and getAll to be definitive.
// Some mocking
const _receivableService = {
newTenantDebitCredit: (tc) => {
console.log('inside newTenantDebitCredit')
return Rx.Observable.of({prop1:'someValue'})
},
getAll: (options) => {
console.log('inside getAll')
return Rx.Observable.of({prop2:'anotherValue'})
}
}
const tenantCredit = {}
// Test
_receivableService.newTenantDebitCredit(tenantCredit)
.take(1)
.switchMap(result => {
console.log('result', result)
return _receivableService.getAll({
refresh: true,
where: { leaseId: this.leaseId }
})
.take(1)
.map(() => result)
})
.take(1)
.subscribe(
(receivables) => {
console.log('receivables', receivables)
//this.refreshBool = !this.refreshBool;
},
(error) => {
console.error(error);
}
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.js"></script>
First of all, this has nothing to do with the switchMap operator.
Normaly removing the take(1) would cause this behaviour. In this case it wouldn't because it itsn't a so called hot observable.
The problem is that you are using a http.post. This is a cold observable which means it will only return a value once. That is also the reason why you don't need to unsubscribe. It will NEVER fire twice. Possible sollutions might be:
Using web sockets to get realtime data.
Creating a timer which will periodically fetch the data.
Simply get the data again whenever you need it.
The way you are asking the question
How can I make the getAll request in the switch map run every time the newTenantDebitCredit method is called above it?
actually sounds to me as if you are calling only newTenantDebitCredit from somewhere in your code, expecting the second request to happen; so I think this might be a misunderstanding of how observable chains work. Let's make an example:
const source$ = Observable.of(42);
source$
.map(value => 2 * value)
.subscribe(console.log);
source$
.subscribe(console.log);
What would you expect this to log? If your answer is "It would log 84 twice", then that is wrong: it logs 84 and 42.
Conceptually, your situation is the same. The second request only happens when the observable returned by newTenantDebitCredit() emits; it will not happen anytime some caller calls newTenantDebitCredit. This is because observable chains do not mutate an observable in-place, they only ever return a new observable.
If you want the second request to happen, you have to actually change the definition of the newTenantDebitCredit method to return an observable set up to perform the second request; alternatively, set up a chained observable that you subscribe to instead of calling newTenantDebitCredit.
Not really an answer but I did solve my problem. It will almost certainly be of no use to anyone BUT it was an issue in the receivableService it was not properly cheeking the boolean: refresh and was pulling values from cache after the first time.