@healthtree/firestore-join
TypeScript icon, indicating that this package has built-in type declarations

0.9.0 • Public • Published

Firestore Join

Easily include references inside your document.

Supports:

  • Single references
  • Arrays of references
  • Maps pointing references
  • Infinite nested references
  • Cache so you don't include .get() the same reference more than once.

npm i firebase-join

How to use:

1 - Install:

npm i firebase-join

2 - Import:

import { SerializedDocumentArray } from '@healthtree/firestore-join';

The building blocks for this library are SerializedDocument and SerializedDocumentArray classes & IncludeConfig.

//Only recommended methods and properties are documented in this interface
interface SerializedDocument{
  // Data returned after calling snapshot.data() and transforming the data, by default it converts firestore timestamps into JS dates.
  data: any 

  // Firestore document reference
  ref: firebase.firestore.DocumentReference 

  // Any included documents, as SerializedDocuments.
  included: Object = {} 

  // Promises for each included reference, this promises resolves once the document is returned by the server or cache.
  promises: Object = {} 
  
  // DocumentSnapshot of the document if document came from server.
  snapshot: DocumentSnapshot

  constructor(snapshot: DocumentSnapshot, includeConfig: IncludeConfig = {}) {
  ...
  }
  
  // Create and return a SerializedDocument that doesn't exist on firestore, useful to keep consistency.
  static createLocal = (ref: DocumentReference, data: any = {}, includeConfig: IncludeConfig = {}): SerializedDocument => {
  ...
  }

  // Gets the document reference and returns a SerializedDocument with any includeConfig
  static fromDocumentReference = (ref: DocumentReference, includeConfig: IncludeConfig): SerializedDocumentPromise => {
  ...  
  }
}
// SerializedDocumentArray is basically an array of SerializedDocuments
// It implements a ready function to know when all included documents are ready

interface SerializedDocumentArray extends Array<SerializedDocument> {
    constructor(querySnapshot: QuerySnapshot, includesConfig: IncludeConfig) {
        
    }

    static fromDocumentReferenceArray = (documentReferenceArray: [DocumentReference], includesConfig: IncludeConfig): SerializedDocumentArrayPromise => {
    ...
    })
}
    // Returns a promise that resolves a SerializedDocumentArray
    // when the documents (without includes) are ready.
    static fromQuery = (query: Query, includesConfig: IncludeConfig): SerializedDocumentArrayPromise => {
    ...
    }
    // Returns a promise that resolves when all included documents are ready
    ready() {
    
    }
}

To serialize an array of documents without including any references.

const posts = await SerializedDocumentArray.fromQuery(firestore.collection('posts'));

// or with any firestore supported filters

const postsFromUser = await SerializedDocumentArray.fromQuery(firestore.collection('posts').where('user','==', userReference));

To serialize an array of documents including a reference and waiting for all the included references to be ready.

Let's pretend each post has a property called user, where user is a documentReference of the user that created the post.

To include all the users, you pass an includeConfig object as the second parameter and call a ready function that returns a promise that resolves once all the references are resolved.

const posts = await SerializedDocumentArray.fromQuery(
  firestore.collection('posts'), 
  {user: true}
).ready();

// with any firestore supported filters
const postsFromUser = await SerializedDocumentArray.fromQuery(
  firestore.collection('posts').where('user','==', userReference),
  {user: true}
).ready();

To serialize an array of documents including a reference and waiting for all the included references to be ready.

Let's pretend each post has a property called user, where user is a documentReference of the user that created the post.

To include all the users, you pass an includeConfig object as the second parameter and call a ready function that returns a promise that resolves once all the references are resolved.

const posts = await SerializedDocumentArray.fromQuery(
  firestore.collection('posts'), 
  {user: true}
).ready();

// with any firestore supported filters
const postsFromUser = await SerializedDocumentArray.fromQuery(
  firestore.collection('posts').where('user','==', userReference),
  {user: true}
).ready();

// with nested references to include
const posts = await SerializedDocumentArray.fromQuery(
  firestore.collection('posts'),
  {user: {organization: true}}
).ready();


// You can access the included documents
console.log(posts[0].includes.user); // User data
console.log(posts[0].includes.user.includes.organization.data); // User->Organization data

// if included documents are an array
const posts = await SerializedDocumentArray.fromQuery(
  firestore.collection('posts'),
  {
    user: true,
    tags: true
  }
).ready();

// You can access the included documents
console.log(posts[0].includes.tags); // Array of SerializedDocuments containing all tags

Listen to firestore query snapshots and serialize

// Only use if your use case really justifies real time updates.
let posts;
 firestore.collection('posts').onSnapshot(async querySnapshot => {
   posts = await new SerializedDocumentArray(querySnapshot, {user: true}).ready()
})

When to use createLocal?

Let's pretend we have a page/component used create or edit a document in firestore.

// Sample using svelte
onMount(async () => {
  const postId = $page.query.postId;
  let post;
  if(postId === 'new') {
    // Pass the desired reference and any initial data
    post = SerializedDocument.createLocal(db.collection('posts').doc(), {user: userReference})
  } else {
    post = await SerializedDocument.fromDocumentReference(db.collection('posts').doc(postId))
  }
})

// UI to modify the message on post, no need for double ui if post is new

onSave = () => {
  post.ref.set(post.data); 
  // No need to have extra logic to see if it was a new doc
}

Advanced - Include documents but listen to individual include to be ready.

const posts = await SerializedDocumentArray.fromQuery(
  firestore.collection('posts'), 
  {user: true}
);

// We are not going to wait for includes to be ready, start rendering and only render user name when ready.

// Sample using svelte

{#each posts as post (post.ref.id)}
<div>
  {#await post.promises.user}
  <p>...waiting</p>
  {:then user}
  <p>From: {post.included.user.data.name}</p>
  {:catch error}
  
  {/await}
  {post.data.message}
</div>
{/each}

//This allows for fast, progressive ui rendering 
// where you don't have to wait for everything to be ready

Package Sidebar

Install

npm i @healthtree/firestore-join

Weekly Downloads

148

Version

0.9.0

License

MIT

Unpacked Size

429 kB

Total Files

12

Last publish

Collaborators

  • sahagun-jorge
  • alvarocapde
  • healthtree-admin
  • diegofhe
  • jpcapdevila
  • aureliob