Vue Sync documentation
# What is Vue Sync
Vue Sync is a library that brings a unified syntax for reading and writing data and allows you to easily work with this data in your app like a "global" store.
You can power Vue Sync with "store plugins" that work in the background to keep this data automatically in sync with whatever service/database or local store you use.
## Benefits of Vue Sync
- an easy to use global store
- unified syntax for reading and writing data
- integration of cloud services and databases (like Firebase, MongoDB, ...)
- integration of local state libraries (like Vuex, Redux, ...)
- your data automatically stays in sync with external services
- you do not need to learn each service's SDK
- API calls are optimised and handled for you (to be as economic as possible)
- optimistic UI built in by default (can be opt out of)
- dynamic modules for maximum flexibility in your data structure
- small footprint by using only the store plugins you need
- (TS users) type safety and helpers when reading/writing data
> It's like graphQL but much simpler with CRUD-like functions
## Store plugins
In most cases you use Vue Sync with two store plugins installed:
- A "local" store plugin, for the data you need stored in memory while using the app. (like Vuex or a simple global object)
- A "remote" store plugin, for the remote data stored. (like Firestore or any other database)
**When reading data:** the remote store will fetch the data; the local store will then add that data for you, so you can easily access and use it in your app.
**When writing data:** both the local and remote stores will update the data for you. You can choose if you want to show updates immidiately and sync remote store in the background (optimistic UI) or wait for the remote store to complete before its reflected locally.
## Modules
Will be organised and accessible through modules. A module could be a single object (a "document") or an map of objects (a "collection").
Modules are dynamically created and destroyed when you read/write data! JavaScript will automatically garbage-collect modules that are not referenced anymore.
## Collection vs document modules
A "module" is something that points to a certain chunk of data in your database.
A module instance has these four things: (1) functions to read data (2) functions to write data (3) the data itself (4) other meta data.
Every module can be one of two types:
- **document:** has a single object as data
- **collection:** has an map of objects as data
Collections can hold several documents, and documents can have sub-collections with more documents. Like so:
```javascript
collection/document/collection/document
```
As an example: `pokedex/001/moves/leafAttack`
- `pokedex` & `moves` are "collections"
- `001` & `leafAttack` are "documents"
:::spoiler Further reading
Collections and documents was inspired by Google Firebase's database called **Firestore**.
If you are still new to this concept you should also read [their documentation on the "data model"](https://firebase.google.com/docs/firestore/data-model).
:::
# Access a module and its data
You can access modules with a simple `collection(id)` & `doc(id)` syntax.
```javascript
// access the collection:
vueSync.collection('pokedex')
// access a sub-doc via its collection:
vueSync.collection('pokedex').doc('001')
// access a sub-doc via its path:
vueSync.doc('pokedex/001')
```
## Access the module data
With the collection or document instance, you can then access the `data`, `id` and several functions on of this "module". Eg.
```javascript
// a collection module
const pokedex = vueSync.collection('pokedex')
pokedex.id // === 'pokedex'
pokedex.data // === Map<'001': { name: 'bulbasaur' }, ... >
// a document module
const bulbasaur = vueSync.doc('pokedex/001')
bulbasaur.id // === '001'
bulbasaur.data // === { name: 'bulbasaur' }
```
:::spoiler ~~The data is also accessible directly like so:~~ (TBD)
```javascript
const pokedex = vueSync.collection('pokedex')
pokedex['001'] // === { name: 'bulbasaur' }
pokedex['001'].name // === 'bulbasaur'
```
:::
↑
To be decided
As an added layer of protection, **any data accesses is readonly!** To "mutate" the data, you'll have to use the functions that are provided (which we explain down below).
This is good because any mutation is supposed to go through a function so it can be synced to all your stores.
# Add & edit data
In the following examples we use this collection instance:
```javascript
export const pokedex = vueSync.collection('pokedex')
```
You can instanciate modules and then export them for to use in other files. (more on this here...) ← *add link*
## Insert a new document
### Insert with random ID
When you insert a new document without specifying an ID, you can do so by calling `insert` directly on the collection:
```javascript
const data = { name: 'squirtle' }
// insert a new document with random ID
const newDoc = await pokedex.insert(data)
```
`insert` returns a newly created instance of a document-module. You can then retrieve both the generated `id` and the `data` from this.
### Insert with pre-set ID
If you want to provide an ID yourself, you can do so by calling `insert` on the doc instance of that preset id:
```javascript
await pokedex.doc(presetId).insert(data)
```
In the case of using a presetId, it will return `Promise<void>`.
:::spoiler How can I batch insert documents?
You can make a simple loop. **Vue Sync will automatically optimise** so that only a single "batch" API call is made!
```javascript
const newPokemon = [
{ name: 'Flareon' },
{ name: 'Vaporeon' },
{ name: 'Jolteon' },
]
for (const data of newPokemon) {
pokedex.insert(data)
}
```
:::
## Delete a document
```javascript
await pokedex.doc('001').delete()
```
:::spoiler How can I batch delete documents?
You can make a simple loop. **Vue Sync will automatically optimise** so that only a single "batch" API call is made!
```javascript
const idsToDelete = ['001', '004', '007']
for (const id of idsToDelete) {
vueSync.doc(id).delete()
}
```
:::
## Delete a document prop
You can delete a single prop or an array of props of a document:
```javascript
await pokedex.doc('001').deleteProp('name')
// or
await pokedex.doc('001').deleteProp(['name', ...])
```
You can also delete nested props by using the "dot" notation:
```javascript
await pokedex.doc('001').deleteProp('moves.tackle')
// will remove 'tackle' from an object called 'moves'
```
## Modify a document
There are 3 ways to modify documents:
- merge
- assign
- replace
### Merge
Merge will update your document and deep-merge any nested properties.
```javascript
const bulbasaur = pokedex.doc('001')
// bulbasaur.data === { name: 'bulbasaur', types: { grass: true } }
await bulbasaur.merge({ types: { poison: true } })
// bulbasaur.data === { name: 'bulbasaur', types: { grass: true, poison: true } }
```
In the example above, Bulbasaur kept `grass: true` when `{ poison: true }` was merged onto it.
### Assign
Assign will update your document and shallow-merge any nested properties. That means that nested objects are replaced by what ever you pass.
```javascript
const bulbasaur = pokedex.doc('001')
// bulbasaur.data === { name: 'bulbasaur', types: { grass: true } }
await bulbasaur.merge({ types: { poison: true } })
// bulbasaur.data === { name: 'bulbasaur', types: { poison: true } }
```
In the example above, Bulbasaur lost `grass: true` when `{ poison: true }` was assigned onto it.
### Replace
Replace will replace the entire object of your document with whatever you pass.
```javascript
const bulbasaur = pokedex.doc('001')
// bulbasaur.data === { name: 'bulbasaur', types: { grass: true } }
await bulbasaur.replace({ level: 16 })
// bulbasaur.data === { level: 16 }
```
In the example above, Bulbasaur **lost** all of its data and it was replaced with whatever was passed.
# 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 get data once
- Set up a "stream" to receive realtime updates
## Get data once
When you get data by executing `get()`, the data will be fetched from a server by your "remote" store plugin and then added to your module's data by your "local" store plugin.
### Get a single document
When you create a module instance of a document, but the data is still on the server, you can retrieve it with `get()` like so:
```javascript
const bulbasaur = vueSync.doc('pokedex/001')
// bulbasaur.data === {} // empty!
await pokedex.get()
// now it is available locally:
const bulbasaurData = bulbasaur.data
// bulbasaurData === { name: 'Bulbasaur' }
```
### Get multiple documents in a collection
```javascript
const pokedex = vueSync.collection('pokedex')
// pokedex.data === Map<> // empty!
// pokedex.data.values() === [] // empty!
await pokedex.get()
// now they are available locally:
// pokedex.data === Map<'001': { name: 'Bulbasaur' }, ...>
const allPokemon = pokedex.data.values()
// allPokemon === [{ name: 'Bulbasaur' }, ...]
```
## Stream realtime updates
When you set up a "stream" for a document or collection, just like `get()`, your the data will be fetched from a server by your "remote" store plugin and then added to your module's data by your "local" store plugin.
Afterwards, if there are any changes to this document, they will automatically be updated in your "local" data while the stream is open.
### Stream a collection
```javascript
const pokedex = vueSync.collection('pokedex')
// open the stream
pokedex.stream()
// data that comes in from the remote store will be added to your data and stays in sync
// now they are available locally:
// pokedex.data === Map<'001': { name: 'Bulbasaur' }, ...>
const allPokemon = pokedex.data.values()
// allPokemon === [{ name: 'Bulbasaur' }, ...]
```
### Stream a single document
```javascript
const bulbasaur = pokedex.doc('001')
// open the stream
bulbasaur.stream()
// data that comes in from the remote store will be added to your data and stays in sync
const data = bulbasaur.data
// data === { name: 'Bulbasaur' }
```
:::hint
The promise returned from a stream is only resolved if the stream channel was closed. Either by the dev or because of an error (in which case it will reject). As long as the stream is open, the promise returned will not resolve.
:::
### Closing a stream
You can list all open streams like so:
```javascript
vueSync.collection('pokedex').openStreams.entries()
// or for a doc:
vueSync.doc('pokedex/001').openStreams.entries()
```
`openStreams` is a [Map](link to mdn) with the payload you passed to start the stream as key, and the "close" function as value.
Here is a full example of opening and closing a stream:
```javascript
// open the stream:
const streamOptions = {}
vueSync.collection('pokedex').stream(streamOptions)
// close the stream:
const streamCloseId = streamOptions
const closeFn = vueSync.collection('pokedex').openStreams.get(streamCloseId)
closeFn()
```
Note if you didn't pass any options when you opened the stream, like so: `stream()` then it's "id" in the openStreams map will be `undefined`.
## Using clauses when reading (filters, limits, order...)
You might only want to get or stream _some_ documents, filtered by a field; limited to X docs; or in a certain order.
MORE DOCUMENTATION COMING ON THIS SOON
You can pass an object with options to `get()` and `stream()` like so:
```javascript
const clauses = { /* some options */ }
await vueSync.collection('pokedex').get(clauses)
```
More information on what kind of options you can pass can be found in the documentation of your store plugin.