Read Data
There are two ways to retrieve data from your remote stores. Either of these methods can be used with documents and collections.
- Execute a function to fetch data once
- Set up a "stream" to receive realtime updates
Fetch Data Once
When you get data by executing fetch()
, the data will be fetched from a server by your "remote" store plugin and then added to your module's data by your "cache" store plugin.
For displaying fetched data in the DOM see the Displaying data in the DOM.
Fetch a Single Document
When you call fetch()
on a document module, your remote store will go and fetch the document from your database and add it to your local cache store.
const bulbasaur = magnetar.doc('pokedex/001')
// bulbasaur's data is not yet in cached data
// bulbasaur.data ≈ {}
await bulbasaur.fetch()
// now it is available:
const data = bulbasaur.data
// bulbasaur.data ≈ { name: 'Bulbasaur' }
The fetch
action returns whatever data was fetched. When fetching a single document, the data will be an object.
// create the module and immediately use the fetched data
const bulbasaurData = await magnetar.doc('pokedex/001').fetch()
// bulbasaurData ≈ { name: 'Bulbasaur' }
Fetching is optimistic by default!
const bulbasaur = magnetar.collection('pokedex').doc('001')
await bulbasaur.fetch()
// ⤷ does nothing if already fetched once
await bulbasaur.fetch({ force: true })
// ⤷ makes API call to remote store
Fetch Multiple Documents
const pokedexModule = magnetar.collection('pokedex')
// pokedexModule.data ≈ Map<> // empty!
// pokedexModule.data.values() ≈ [] // empty!
await pokedexModule.fetch()
// now they are available locally:
// pokedexModule.data ≈ Map<
// '001': { name: 'Bulbasaur' },
// /* etc...*/
// >
const allPokemon = pokedexModule.data.values()
// allPokemon ≈ [{ name: 'Bulbasaur' }, /* etc...*/ ]
The fetch
action returns whatever data was fetched. When fetching a collection, the data will be a Map ?.
// create the module and immediately use the fetched data
const pokedexData = await magnetar.collection('pokedex').fetch()
// pokedexData ≈ Map<
// '001': { name: 'Bulbasaur' },
// /* etc...*/
// >
Fetching is optimistic by default!
const pokedex = magnetar.collection('pokedex')
pokedex.fetch()
// ⤷ does nothing if already fetched once
pokedex.fetch({ force: true })
// ⤷ makes API call to remote store
Fetch and get document data
You can fetch and immediately return a document's data. This is optimistic by default — meaning it will only make an API call once and otherwise return the already fetched data. Add the force
option to force multiple fetch calls.
// this will only fetch the data once, and from there on always return the already fetched data:
const bulbasaurData = await magnetar.collection('pokedex').doc('001').fetch()
// every time this is executed, it will (re)fetch and return the data:
const bulbasaurData = await magnetar.collection('pokedex').doc('001').fetch({ force: true })
// after fetching the data is also available at `.data`:
magnetar.collection('pokedex').doc('001').data
Fetch Collection Count
You can also fetch the count of documents in a collection:
const count = await magnetar.collection('pokedex').fetchCount() // 151
// after fetching, the count is also available at `.count`:
magnetar.collection('pokedex').count // 151
Fetch the Sum of a field
You can also fetch the sum of a field in a collection:
const sum = await magnetar.collection('pokedex').fetchSum('base.HP') // 9696
// after fetching, the sum is also available at `.sum`:
magnetar.collection('pokedex').sum // { base: { HP: 9696 } }
Fetch an Average of a field
You can also fetch the average of a field in a collection:
const average = await magnetar.collection('pokedex').fetchAverage('base.HP') // 64.2
// after fetching, the average is also available at `.average`:
magnetar.collection('pokedex').average // { base: { HP: 64.2 } }
Stream Realtime Updates
When you set up a stream for a document or collection, just like fetch()
, your the data will be fetched from a server by your remote store plugin and then added to your module's cache data.
Afterwards, any changes to this document remotely will automatically be reflected in your module's cache data while the stream is open.
Please note: a streaming promise will never resolve as long as your stream is open! There is no way to know when or how many documents will be loaded in, as this depends on your remote store.
For displaying streamed data in the DOM see the Displaying data in the DOM.
Firestore vs Magnetar
The Firestore JS SDK has a built-in feature for realtime updates via a method called onSnapshot
. The main pain points are:
- The syntax you have to use is very convoluted and complex
- If your not careful, you can open the same stream twice and they both will use memory
- You need to write a lot of code to capture the documents that comes in and save them in local cache
- You need to organize where and how to temporarily save the function you get back to stop the stream
Magnetar's Firestore Plugin uses onSnapshot
under the hood but does these things for you:
- The syntax is super clean. It's just
.stream()
- A stream cannot be opened twice on accident, the already open stream will be returned in case you open it again
- No need to write any extra code, Magnetar captures the documents that comes in and saves them in local cache for you
- No need to keep around the function to stop the stream, Magnetar does this for you
Stream a Collection
const pokedexModule = magnetar.collection('pokedex')
// open the stream
pokedexModule.stream().catch((error) => {
// the stream was closed because of an error
})
// ... some time later
// incoming data from the remote store will be
// added to your module's data and stay in sync
// pokedexModule.data ≈ Map<
// '001': { name: 'Bulbasaur' },
// /* etc...*/
// >
// pokedexModule.data.values() ≈ [{ name: 'Bulbasaur' }, /* etc...*/ ]
That's it! You don't need any other logic!!
Stream a Single Document
const bulbasaur = magnetar.doc('pokedex/001')
// open the stream
bulbasaur.stream().catch((error) => {
// the stream was closed because of an error
})
// ... some time later
// incoming data from the remote store will be
// added to your module's data and stay in sync
// bulbasaur.data ≈ { name: 'Bulbasaur' }
That's it! You don't need any other logic!!
Closing a Stream
You can close a stream again simply by executing closeStream
on the same module:
const pokedexModule = magnetar.collection('pokedex')
// close the collection stream:
pokedexModule.closeStream()
// close the doc stream:
pokedexModule.doc('001').closeStream()
The Streaming Promise
You don't need to await
a stream, because the promise won't resolve as long as its open!
// open the stream
await magnetar.collection('pokedex').stream()
// The code here will only get executed after the stream was closed!
console.log('closed!')
Instead, for a stream, it might be better to just use .then()
and .catch()
// open the stream
magnetar
.collection('pokedex')
.stream()
.then(() => {
// the stream was closed via `closeStream()` !
})
.catch(() => {
// the stream was closed because of an error! !
})
// The code here will get executed immediately
console.log('The stream was opened!')
Query Data (filter, order by, limit...)
There are founr methods to query more specific data in a collection:
where
orderBy
limit
query
You can execute and chain these methods on collections to create a queried module that is just like a regular module but with your query applied.
When you apply a query it affects both the remote and cache stores:
- If you make a
fetch()
call with a queried module, it will pass the queries to the remote store, which will make sure that your query is applied to the API call. - If you access module data with a queried module, the local cache store will also make sure that your query is applied to whatever data it returns.
const pokedexModule = magnetar.collection('pokedex')
const fireTypePokemon = pokedexModule.where('type', '==', 'fire')
await fireTypePokemon.fetch()
fireTypePokemon.data.values() // returns just the queried docs that were fetched
pokedexModule.data.values() // returns all docs fetched so far, including those fire types you just fetched
Where
where
needs 3 parameters.
- the prop name
- the operator
- the value to compare with
The possible operators include:
'=='
'!='
'<'
'<='
'>'
'>='
'array-contains'
'array-contains-any'
'in'
'not-in'
Eg. "all Fire Pokemon above level 16"
magnetar
.collection('pokedex')
.where('type', '==', 'fire')
.where('level', '>', 16)
For now read the Firestore documentation on Simple and Compound Queries. The concept is inspired by Firestore, but with Magnetar every cache and remote store plugin implements the proper logic to work with these kind of queries!
More Magnetar specific information on this will come soon.
Query
If you need OR
in your where
queries, you need to use .query
:
magnetar
.collection('pokedex')
.query({
or: [
['name', '==', 'flareon'],
['evolution', '==', 'flareon']
]
})
You can also combine or
s and and
s:
magnetar
.collection('pokedex')
.query({
or: [
{ and: [['type', '==', 'fire'], ['level', '>', 16]] },
{ and: [['type', '==', 'water'], ['level', '<', 16]] }
]
})
Order by
You can order docs coming in by a specific field, ascending or descending. orderBy
needs 2 parameters.
- the prop name to order by
'asc'
or'desc'
Eg. "all Pokemon sorted alphabetically"
magnetar
.collection('pokedex')
.orderBy('name', 'asc')
Limit
Limit is mainly for the remote store to limit the amount of records fetched at a time. limit
needs 1 parameter.
- The count to limit by
Eg. "get the first 10 Pokemon sorted alphabetically"
magnetar
.collection('pokedex')
.orderBy('name', 'asc')
.limit(10)
More Examples
You can combine where
with orderBy
:
magnetar
.collection('pokedex')
.where('type', '==', 'fire')
.orderBy('name', 'asc')
You can either stream
or fetch
a queried module:
const fireTypePokemon = magnetar
.collection('pokedex')
.where('type', '==', 'fire')
.orderBy('name', 'asc')
fireTypePokemon.fetch()
// or
fireTypePokemon.stream()
Here is a complete example for a search function:
const pokedexModule = magnetar.collection('pokedex')
/**
* @param {string} type - the Pokemon Type that's being searched
* @returns {Record<string, any>[]}
*/
async function searchPokemon(type) {
const queriedPokedex = pokedexModule.where('type', '==', type).orderBy('name', 'asc')
await queriedPokedex.fetch()
// return all Pokemon for just this query
return queriedPokedex.data.values()
// OR
// return all Pokemon fetched so far (eg. over multiple searches)
// ↓
// return pokedexModule.data.values()
}
Query Limitations
Based on your remote store plugin there might be limitations to what/how you can query data.
For Cloud Firestore be sure check the Query Limitations in their official documentation.
Opening & Closing Multiple Streams
You can have multiple streams open with different queries.
const pokedexModule = magnetar.collection('pokedex')
// open two streams with different filters:
pokedexModule.where('type', '==', 'fire').stream()
pokedexModule.where('type', '==', 'water').stream()
// access the documents from the two streams separately:
pokedexModule.where('type', '==', 'fire').data.values()
pokedexModule.where('type', '==', 'water').data.values()
// access all the documents at once:
pokedexModule.data.values()
// close the streams once by one:
pokedexModule.where('type', '==', 'fire').closeStream()
pokedexModule.where('type', '==', 'water').closeStream()
// close all streams at once:
pokedexModule.closeAllStreams()
Collection Groups (WIP)
This chapter is still being written
If you need to query data from multiple sub modules you can do so by using a Collection Group.
const waterAttacks = magnetar.collectionGroup(`pokedex/*/attacks`).where('type', '==', 'water')
waterAttacks.fetch()
This syntax is not fully implemented yet!