What should happen when you complete a lesson? It would be nice to track the completeness of a session. We can later extend this by tracking the time it took to complete a lesson – but first we need a strategy.
Data Model
We can’t add the progress to the actual lesson. We need to keep that information separated. We also want to ensure the access to the information is performant and the solution scales.
As every user has a unique identifier and each lesson has a unique identifier and the easiest way to select data in Firebase is by a unique path, why not use this information?
This is a concept that was a bit strange for me at first, but the concept of sub collections is pretty powerful. I can create a user profile using the user’s unique identifier and then keep all the completed lessons in a sub collection. This makes it very easy to select (and protect!) the data.
Save Data
At the end of the lesson we just save the data like so:
completeLesson(lesson: Lesson): Observable<void> { return this.fireAuth.user.pipe( switchMap((user) => { return this.firestore .collection('user-data') .doc(user.uid) .collection('lessons') .doc(lesson.id) .set({ complete: true, }); }) ); }
Read Data
When reading the lessons we should also read the user’s lesson history. This alone would be pretty straight forward. But we somehow need to combine / join the two collections.
For this I extended the lesson model with a new boolean which I set to true if the lesson has been solved. I’ have to think about a more efficient way, but I think it is fully reactive this way.
selectLessons(): Observable<Lesson[]> { return this.fireAuth.user.pipe( switchMap((user) => { return combineLatest([ this.firestore .collection<Lesson>('lessons') .valueChanges({ idField: 'id' }), this.firestore .collection('user-data') .doc(user.uid) .collection('lessons') .valueChanges({ idField: 'id' }), ]); }), map(([lessons, myLessons]) => { lessons.forEach((lesson) => { lesson.complete = false; // reset required to be consistent in case the user data has been changed myLessons.forEach((myLesson) => { if (myLesson.id === lesson.id) { lesson.complete = true; } }); }); return lessons; }) ); }
Don’t forget the Rules
Every user should be able to read / write their own data. I don’t care about the data format / validation for now, if the user destroys it by intention – their bad. My only concern would be that a malicious user would store big documents. Let’s see later if we can fix this somehow. But for now…
If you are unsure (and specifically if you have rules like above) it makes sense to at least test them quickly with the integrated playground (positive and negative 🙂).
What next?
Next let’s make everything nice and tidy:
- nice forms
- nice traning mode
Last but not least:
- provide a simple editor
- cleanup the code
- check for memory leaks (which usually happens if you subscribe to data and forget to unsubscribe when the component is destroyed)
- write tests! It’s good practice for private projects as well.