Andrey Lechev
Index Of /

Index Of /

Firestore as a REST API

For a blog, I don't need a full Firestore integration. The updates are not happening often. It'll be enough just to fetch the articles once they are needed. And so I did.

Composing URLs for the requests

First, I looked at the documentation. The URLs are too long and a bit weird, so I did a function getFirestoreRequestUrl to build the URL using the Firebase Config and collection and/or document name.

import firebaseConfig from './firebase-config.js'; 

const dbHost = 'https://firestore.googleapis.com/v1/'; 
const dbUri = `projects/${firebaseConfig.projectId}/databases/(default)/documents/`; 

const defaultOrder = 'orderBy=date+desc&'; 

export function getFirestoreRequestUrl(collection, docId = '') { 
  const order = docId !== '' ? '' : defaultOrder; 
  return `${dbHost}${dbUri}${collection}/${docId}?${order}key=${firebaseConfig.apiKey}`; 
}

Fetching the data

Now I am able to use it in the preloader with this.fetch() or just the browser's fetch() (or even XMLHttpRequest if you need to support IE).

It can be easier if the fetch method was available on the server as well on the client to use it in the supporting function, of course.

Here's the example of getting the list of all articles (with a few articles in the blog I don't need any filters yet):

<script context="module"> 
export async function preload({ params, query }) { 
  return this.fetch(getFirestoreRequestUrl('articles')).then(res => res.json()) 
    .then(res => { 
      return { articles: res.documents } 
    }).catch(err => { 
      throw error(err.status, err.message); 
    }); 
} 
</script> 

<script> 
import { onMount } from 'svelte'; 
import { getRequestUrl } from '../firebase.js'; 

export let articles; 

onMount(() => { 
  fetch(getFirestoreRequestUrl('articles')).then(res => res.json()) 
    .then(res => { 
      articles = res.documents; 
    }).catch(err => { 
      throw error(err.status, err.message); 
    }); 
}) 
</script>

Answer format

The problem here is with the unexpected answer format — every value is wrapped in an additional object with property reflecting the data type, and complex types like dates and arrays and maps are buried even deeper:

{ 
  documents: [ 
    { 
        name: '.../(default)/documents/articles/my-first-vuexperience', 
        fields: { 
          title: { stringValue: 'My First Vuexperience' }, 
          body: { stringValue: '<p>For the last six weeks...' }, 
          scripts: { 
            arrayValue: { 
              values: [ 
                { stringValue: 'https://...' }, 
                ... 
              ] 
            } 
          } 
        }, 
        createTime: '2019-06-02T22:55:20.072237Z', 
        updateTime: '2019-06-03T13:35:10.754524Z' 
    }, 
    ... 
  ] 
}

It's simply unusable this way. We have to...

Convert values into normal JSON

To convert the answer I wrote a function that parses all the subtree of values:

... 
export function parseFirestoreResults(docs) { 
  // extract the values 
  function getValue(field) { 
    if (field.mapValue) { 
      const mapFields = {}; 
      Object.keys(field.mapValue.fields).forEach(name => { 
        mapFields[name] = getValue(field.mapValue.fields[name]); 
      }); 
      return mapFields; 
    } else if (field.arrayValue) { 
      return field.arrayValue.values.map(item => getValue(item)); 
    } else { 
      return Object.values(field)[0]; 
    } 
  } 

  // iterate the document fields 
  function getValues(data) { 
    const fields = {}; 
    Object.keys(data).map(name => { 
      fields[name] = getValue(data[name]); 
    }); 
    return fields; 
  } 

  // iterate documents and return transformed object 
  return docs.map(doc => { 
    return { 
      id: doc.name.substr(dbUri.length), 
      ...getValues(doc.fields) 
    }; 
  }); 
}

Final Touches

The only thing that's left is to use the function to parse the results when fetching the data:

<script context="module"> 
export async function preload({ params, query }) { 
  return this.fetch(getFirestoreRequestUrl('articles')).then(res => res.json()) 
    .then(res => { 
      return { articles: parseFirestoreResults(res.documents) } 
    }).catch(err => { 
      throw error(err.status, err.message); 
    }); 
} 
</script> 
...

That's it. Working perfectly. Chunk size went down to less than 3 KB!

Changes can be seen in the GitHub repo:

 
Share this