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.