AngularFire2 - Joining members and users - firebase

I am trying to list users conversations in an AngularFire2 app, which strucutre is the following:
chats
"chat1": {
title: "First chat",
lastMessage: "Hello world",
members: {
"user1": true,
"user2": true,
}
}
users
"user1": {
name: "Ben",
surname: "Bennsay"
}
"user2": {...}
I am trying to map and list chats in a way that i can easily display the chats participants names bellow the last message.
Question 1: This example differs a little bit from then official recommendation but i feel it would still be valid and scalable. Am i right ?
Question 2: How to actually join members and users to have a users array in my chats list ?
Here is what i have so far.
// retrieve chats "user1" participates in
this.afChatsRef = this.af.database.list(this.datastore(), {
query: {
orderByChild: "/members/user1", // by user id in members
equalTo: true,
}
}).map(chats => {
chats.map(chat => {
// HMMM? WHAT TO DO HERE ?
});
return chats;
});
Thanks, in advance.
UPDATE i have also tried the following, which does not seem quite right (and i cannot access user properties).
this.af.database.list(this.datastore()).map(chats => {
chats.map(chat => {
// chat.users = [];
for (var key in chat.members) {
this.af.database.object("/users/" + key).subscribe(user => {
chat.members[key] = user;
});
}
return chat;
});
console.log(chats);
return chats;
});

You want to return the nested map and fetch the users inside of that. Something like this;
// retrieve chats "user1" participates in
this.afChatsRef = this.af.database.list(...).map(chats => {
// Note the return!
return chats.map(chat => {
// Avoid side effects by storing members separate from the keys
chat.memberData = {};
// Iterate keys and download members
Object.keys(chat.members||{}).forEach(uid => {
// `users` represents a service to cache and load users on demand
chat.memberData[uid] = users.load(uid);
});
});
return chats;
});
Here's a good way to create the users service with caching:
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs/Rx';
import { AngularFireDatabase } from 'angularfire2/database';
#Injectable()
export class UserProvider {
db: AngularFireDatabase;
users: Map<String, Observable<User>>;
constructor(db: AngularFireDatabase) {
this.db = db;
this.users = new Map();
}
load(userid:string) : Observable<User> {
if( !this.users.has(userid) ) {
this.users.set(userid, this.db.object(`members/${userid}`).valueChanges());
}
return this.users.get(userid);
}
}
export interface User {
name:string;
nick:string;
}
And here is a working example of async joins in AngularFire2.

Related

How to join data from two collections by reference field in Firestore?

I have 2 collections in my firestore. One called owners and the second is unicorns.
An owner has only a name field and a unicorn has a name and a reference to the owner.
I want a query to return an array of objects that looks like this
unicorns = { id: 123,
name: Dreamy,
owner: { id: 1
name: John Cane
}
}
my query looks like this but there is something missing that I can't figure out
let unis = [];
db
.collection("unicorns")
.get()
.then((querySnapshot) => {
querySnapshot.forEach((doc) => {
let uni = {
id: doc.id,
name: doc.data().name,
owner: doc.data().owner,
};
if (uni.owner) {
doc
.data()
.owner.get()
.then((res) => {
uni.owner = {
id: res.id,
name: res.data().name,
};
unis.push(uni);
})
.then(() => {
setUnicorns(unis);
})
.catch((err) => console.error(err));
} else {
unis.push(uni);
}
});
})
When I setUnicorns hook and I try to map the results I don't get all the data I need
You cannot run the .get() method on a string. You would have to run a separate request to firestore to get owner documents. I would recommend using a for-of inside a async function as shown below.
const db = admin.firestore()
//Declare the reference for collections before the loop starts
const ownersCollection = db.collection("owners")
const unicornsCollection = db.collection("unicorns")
const ownersData = {}
let unis = [];
unicornsCollection.get().then(async (querySnapshot) => {
for (const doc of querySnapshot) {
let uni = {
id: doc.id,
name: doc.data().name,
//Assuming the owner field is the UID of owner
owner: doc.data().owner,
};
if (uni.owner) {
//Check if owner's data is already fetched to save extra requests to Firestore
if (ownersData[uni.owner]) {
uni.owner = {
id: ownersData[uni.owner].id,
name: ownersData[uni.owner].name,
};
unis.push(uni);
} else {
//User the ownersCollection Reference to get owner information
ownersCollection.doc(uni.owner).get().then((ownerDoc) => {
uni.owner = {
id: ownerDoc.data().id,
name: ownerDoc.data().name,
};
ownersData[uni.owner] = {
id: ownerDoc.data().id,
name: ownerDoc.data().name,
}
unis.push(uni);
}).then(() => {
setUnicorns(unis);
}).catch((err) => console.error(err));
}
} else {
unis.push(uni);
}
}
})
How this works? It fetches all the unicorn documents and iterates over then to check if the document has owner's UID. If yes then runs another request to Firestore to get that unicorn's owner's document. Let me know if you have any questions.
Edit: In case a single owner has multiple unicorns then you surely don't want to fetch the data of same owner again and again. So add that in an object locally and check if the data of that owner is already fetched before making a request to Firestore. Code for the same updated above.

Firestore Angular2 Retrieve documents based on current user

I have started developing a mobile app using IONIC, ANGULAR against Google Firestore. This app will consume mostly documents based on the current user and most of my queries I will need to pass in this user. However, I am experiencing issues getting documents from firestore using the following code from my service to the page:
user-profile.service.ts
async get(){
// await this.afAuth.user.subscribe(currentUser => {
// if(currentUser){
// this.userId = currentUser.uid;
// console.log("User Current ID: " + this.userId);
console.log("PP:" +this.afAuth.auth.currentUser.uid)
return this.userProfilesCollection.doc<UserProfile>(this.afAuth.auth.currentUser.uid).snapshotChanges().pipe(
map(doc => {
const id = doc.payload.id;
const data = doc.payload.data();
return{id, ...data };
}
)
);
}
landing-page.page.ts
export class LandingPage implements OnInit {
userProfile : UserProfile;
constructor(
private authService : AuthService,
private loadingController : LoadingController,
private userProfileService: UserProfileService,
private router : Router
) {
}
ngOnInit() {
this.loadUserProfile();
}
async loadUserProfile() {
const loading = await this.loadingController.create({
message: 'Loading user profile'
});
await loading.present();
this.userProfileService.get().then(res=>{
console.log(res);
loading.dismiss();
})
// this.userProfileService.get().then(
// res =>
// {
// loading.dismiss();
// res.subscribe(c=>
// {
// this.userProfile = {
// cellphone: c.data.cellphone,
// dateOfBirth: c.data.dateOfBirth,
// email: c.data.email,
// firstname: c.data.firstname,
// gender: c.data.gender,
// id: c.data.id,
// lastname: c.data.lastname,
// telephone: c.data.telephone,
// userId: c.data.userId,
// website: c.data.website
// };
// });
// }
// );
}
}
Does anyone have a simple example how to achieve this, and I need to use the load profile to navigate across the application as the currently logged in user will be able to manage their profile and the list items (documents) linked to them?

Firebase Firestore returns a promise in Vue

I'm trying to use some data from from Firestore. before it used to work, now in Vuetify I keep getting 'PENDING' if I try to access the $data.users
export default {
data() {
return {
users: [],
};
},
created() {
db.collection('users').get().then((snapshot) => {
snapshot.forEach((doc) => {
const user = doc.data();
user.id = doc.id;
this.users = user;
console.log(user.documents.selfie.url); // Here the log return the value correctly
});
});
},
methods: {
imageUrl(user) {
console.log(user.documents.selfie.url); // Here the log return "Pending";
},
Inside the template I run a v-for (user, index) in users :key='index'
ERROR:
Uncaught (in promise) TypeError: Cannot read property 'selfie' of undefined
It's difficult to be 100% sure without reproducing your problem, but I think the problem comes from the fact that the Promise returned by the asynchronous get() method is not yet fulfilled when you call the imageUrl() method. This is why you get the pending value.
One possibility to solve that is to check as follows:
methods: {
imageUrl(user) {
if (user) {
console.log(user.documents.selfie.url);
} else {
//...
}
},
Also, is seems you want to populate the users Array with the docs from the users collection. You should do as follows:
created() {
db.collection('users').get().then((snapshot) => {
let usersArray = [];
snapshot.forEach((doc) => {
const user = doc.data();
user.id = doc.id;
usersArray.push(user);
console.log(user.documents.selfie.url); // Here the log return the value correctly
});
this.users = usersArray;
});
},
With your current code you assign the last user in the loop, not the list of users.

put a firestore variable in a polymer template

Maybe I am over thinking it, but I can't figure out a way to put the results of a Firestore query into a Polymer 3 template. For example:
class MyPage extends PolymerElement {
constructor() {
super();
/* somehow set this.users to the firestore users query results */
}
static get properties() {
return {
users: {
type: String
}
}
}
static get template() {
return html`<div>[[users]]</div>`;
}
}
Using the following code, which does work correctly and print to the console:
var all_users;
const setsRef = firestore.collection("users");
setsRef.get().then(function(querySnapshot) {
var users = [];
querySnapshot.forEach(function(doc) {
users.push(doc.data().verb);
});
all_users = users.join(", ");
console.log("All users: ", all_users);
/* run a function here that sets this.users = all_users */
})
.catch(function(error) {
console.log("Error getting documents: ", error);
});
The problem is, I have to run a function from the Firestore results, while the other is a constructor of an object. I would preferably like to have all of my Firebase queries in an external js file, but this async thought process is confusing me.
Use one of the lifecycle methods to load the users:
https://www.polymer-project.org/3.0/docs/devguide/custom-elements#element-lifecycle
class MyPage extends PolymerElement {
constructor() {
super();
this.users = []
}
static get properties() {
return {
users: {
type: String
}
}
}
static get template() {
return html`<div>[[users]]</div>`;
}
async ready() {
super.ready()
try {
const querySnapshot = await firestore.collection("users").get()
this.users = querySnapshot.map(doc => doc.data().verb).join(", ");
} catch (error) {
console.log(error)
}
}
}
If you don't want to use one of the lifescycle methods, then you can fire your own custom event which your MyPage element can listen for: https://www.polymer-project.org/3.0/docs/devguide/events#custom-events
I'm not familiar with Polymer, so the above is untested and what I figure from reading the docs.

Create reducer about user state

I'm trying to apply reflux/ngrx on my current front-end project.
I want to take advantage of this in order to change a slight functionality: Change current user related tasks in order to use a single user state.
Current user related tasks: Currently, I'm using an traditional model in order to achieve user login process... UserService is able to check user credentials. Once it's been checked I store user information on an AppService:
export class LoginComponent implements OnInit {
private fb: FormBuilder;
private form:FormGroup;
private commty: UsersService;
private router: Router;
private appState: AppState;
private alerts: Array<Object>;
constructor()
{
this.alerts = [];
}
ngOnInit():void {
this.form = this.fb.group({
user: ['', Validators.required],
passwd: ['', Validators.minLength(6)]
});
}
public checkPasswd():void {
this.clearAlerts();
this.commty.checkPasswd(this.form.value.mail, this.form.value.passwd)
.subscribe(
(result: any) => {
this.appState.user = result;
this.router.navigate(['/app']);
},
(error: any) => {
this.addAlert(error.message);
}
);
}
private addAlert(message: string): void {
this.alerts.push({type: 'danger', msg: message});
}
public closeAlert(index): void {
this.alerts.splice(index, 1);
};
private clearAlerts(): void {
this.alerts.splice(0, this.alerts.length);
}
}
I'm a bit confused about how to move this code in order to use reflux/ngrx. I'ce read a bit about this topic, nevertheless I'm not quite able to figure out how to move my code. Up to now, I've created an single Store and User interfaces:
store.interface.ts:
export interface IStore {
user: IUser
sources: ISourceRedux;
}
user.interfcae.ts:
export interface IUser {
id: string;
name: string;
username: string;
customer: string;
}
The next step I think I need to do is to create reducers. This step is which I don't quite understand how build this code. Up to now
user.initialstate.ts:
export function initialUserState(): IUser {
return {
id: '',
name: '',
username: '',
customer: '',
sources: []
};
};
user.reducer.ts
export class User {
private static reducerName = 'USER_REDUCER';
public static reducer(user = initialUserState(), {type, payload}: Action) {
if (typeof User.mapActionsToMethod[type] === 'undefined') {
return user;
}
return User.mapActionsToMethod[type](user, type, payload);
}
// ---------------------------------------------------------------
// tslint:disable-next-line:member-ordering
private static mapActionsToMethod = {};
}
Which reducers I should create in order to:
Check credentials.
If credentials are right get this user and update User state store.
If credentials are wrong inform the process has failed.
Perhaps I'm merging concepts... I need some lights...
EDIT
public connect(user: string, currentPasswd: string, extraHttpRequestParams?: any): Observable<UserDTO> {
return this.checkPasswdWithHttpInfo(id, currentPasswd, extraHttpRequestParams)
.map((response: Response) => {
if (response.status === 204) {
return undefined;
} else {
return response.json();
}
}).catch((error: any) => {
if (error.status >= 500) {
return Observable.throw(new Error(error.status));
}
else { //if (error.status >= 400) {
const body = error.json() || '';
const code = body.error || JSON.stringify(body);
const message = body.message || JSON.stringify(body);
return Observable.throw(ApiError.create(code, message));
}
});
}
Ok so this is the next question of your "Integrate ngrx into my code" =).
What you're looking for is : https://github.com/ngrx/effects
The idea behind effects is that an effect let you catch an Action, do side effect (API call or whatever) and you can then dispatch another Action (often success or error).
Flow example to connect a user :
--| [from component] Dispatch action USER_CONNECT
--| [from user.effect.ts]
----| Catch action ofType('USER_CONNECT')
----| Do what you need to do (API call for ex)
----| When the response comes back :
------| If success : Dispatch USER_CONNECT_SUCCESS
------| If error : Dispatch USER_CONNECT_ERROR
Of course when you dispatch either USER_CONNECT_SUCCESS or USER_CONNECT_ERROR you can pass additional data in the payload (for example user information or the error).
Here's a full example :
#Injectable()
export class UsersEffects {
constructor(
private _actions$: Actions,
private _store$: Store<IStore>,
private _userService: UserService,
) { }
#Effect({ dispatch: true }) userConnect$: Observable<Action> = this._actions$
.ofType('USER_CONNECT')
.switchMap((action: Action) =>
this._userService.connect(action.payload.username, action.payload.password)
.map((res: Response) => {
if (!res.ok) {
throw new Error('Error while connecting user !');
}
const rslt = res.json();
return { type: 'USER_CONNECT_SUCCESS', payload: rslt };
})
.catch((err) => {
if (environment.debug) {
console.group();
console.warn('Error catched in users.effects.ts : ofType(USER_CONNECT)');
console.error(err);
console.groupEnd();
}
return Observable.of({
type: 'USER_CONNECT_ERROR',
payload: { error: err }
});
})
);
}
You can take a look into my project Pizza-Sync were I did something similar (except that I don't catch in case of error and do not dispatch if there's an error).

Resources