ngxs-labs / firestore-plugin

Firestore plugin for NGXS
https://ngxs-firebase-plugin.netlify.com/
MIT License
20 stars 14 forks source link

How to Retrieve and Sync Subcollections on Collection Documents #74

Closed cnthiesen closed 1 year ago

cnthiesen commented 1 year ago

Hi! First of, awesome plugin! :D

I have a question regarding keeping my state in sync with subcollections on a collection of documents. I have been trying to figure this out for a few days now and nothing seems to work, except if I change my database structure, which I really want to avoid. I would be truly ecstatic, if I could get an example and figure this out. I think other people as well, maybe :)

I have a collection of workspace documents that each has a "groups" and "membership" subcollection that provides secure information about the rights of members and groups within the workspace. I originally made this structure so I can make Firebase Rules on the subcollections.

Firestore Database Setup

{
 workspaces: (collection) {
  $workspaceId (document): {
   name: "My workspace"
   groups: (subcollection) {
    $groupId: (document) {
     groupRights: 4
   }
   }
   membership: (subcollection) {
    $memberId: (document) {
      memberRights: 4
   }
   }
  }
 }
}

I would ideally like a state of workspaces with their subcollections in sync and available at all times in my application. It would also be fine to have 3 states, that I then combine in a Shared Selector or something. What would be the best approach to accomplish this?

Any help and point in the right direction would be much appreciated!

PS. I tried several things already, most recently I tried to combine with observables and transformed the workspace state to pull the subcollections, but this messes up with the state and causes duplicate workspaces to appear in the state each time there is an update.

Thanks, Best, Chris

markterrill commented 1 year ago

Hi Chris, Ready for this? Don't use Firestore. You're trying to shoe horn your data structure onto firestore's subcollections and it's not suited for that.

The simpler solution to your firebase rules use case is to use firebase realtime database. Reason why is workspace is a single document with all the data within it (including hash arrays). An update to any data element of that single doc will refresh within firebase. Realtime works just the same with rules.

Firestore = cached 'big' documents like a IMDB entry on a movie or an user's app preferences. Realtime = lightweight data like device events and small complex trees for firebase rules

Realistically firestore is like an old school relational database, you want to be really careful about your use case when nesting sub collections. There's a pretty easy 'test' in my mind. Do you only access sub documents on demand or do you actually need all of them, every time you load the parent? If it's the latter it's realistically one document with hash array information you're wanting, so use realtime.

You can use firestore-plugin easily for firebase realtime, just connect your events to a custom function within your xxxx.firestore.ts.

your state file:

this.ngxsFirestoreConnect.connect(EventActions.GetStream, {
  to: ({payload}) => {
    this.logger.debug('EventActions.GetStream connectGetStreamEvents called with params ', payload);
    return this.eventsFS.streamEvents$(payload);
  },
  trackBy: action => action.payload,
  //cancelPrevious: true, //commented out 20230113
});

Your firestore file:

public streamEvents$(id: string): Observable<EventDataIN[] | null> {
  if (id === null || typeof id === 'undefined' || id.length < 3) {
    id = 'xxx-wasnotdefined-xxx'; // won't exist
  }
const instancePath = `devices/${id}/temps`;

this.logger.info('EventActions.GetStream EventsFirestore streamEvents$', instancePath);

const myQuery = query(ref(this.fireRealtime, instancePath), orderByChild('fbdate'), limitToLast(1));
const mylist: Observable<EventDataIN[] | null> = listVal(myQuery, {keyField: 'id'});
return mylist;
}
joaqcid commented 1 year ago

hi @cnthiesen

Subcollections are not easy to deal with. Using the README example, you can access a subcollection but it think this is not what you are looking for.

@Injectable({
  providedIn: 'root'
})
export class ClassificationsFirestore extends NgxsFirestore<Race> {
  protected get path() {
    return `races/${this.raceId}/classifications`;
  }

  private _raceId = '';
  public setRaceId(raceId) {
    this._raceId = raceId;
  }

  protected get raceId() {
    return this._raceId;
  }
}

Another more custom solution would be to create a very specialized query using rxjs concatenating and merging all the data from the collection and its subcollection to finally end up with a single dataset and a single state. Have you tried that?