Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
554 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
# Book Collection example - Vue 3 | ||
|
||
This is a Vue 3 version of [Resgate's Book Collection example](https://github.com/resgateio/resgate/tree/master/examples/book-collection). It contains both a client and a server written for *node.js*. | ||
|
||
The purpose of this example is to show how ResClient can be used together with Vue 3 and its built in reactivity. To learn more about writing services for Resgate, visit the [Resgate project](https://github.com/resgateio/resgate). | ||
|
||
## Prerequisite | ||
|
||
* Have [NATS Server](https://nats.io/download/nats-io/gnatsd/) and [Resgate](https://github.com/resgateio/resgate) running | ||
* Have [node.js](https://nodejs.org/en/download/) installed | ||
|
||
## Running the example | ||
|
||
Run the following commands: | ||
```bash | ||
npm install | ||
npm start | ||
``` | ||
|
||
Open the client | ||
``` | ||
http://localhost:8083 | ||
``` | ||
|
||
## Notes | ||
|
||
In the example, each _ResModel_ and _ResCollection_ is wrapped with `Vue.reactive(...)` to make the Vue components update without the need to listen to specific events. | ||
|
||
```javascript | ||
const app = Vue.createApp({ | ||
client: null, | ||
created() { | ||
// The client is stored outside the data object to prevent it from being | ||
// wrapped in a Vue reactive Proxy. | ||
this.client = new ResClient('ws://localhost:8080') | ||
// The full wildcard (">") let's us wrap all ResModel/ResCollection | ||
// instances in a Vue reactive Proxy. | ||
.registerModelType(">", (api, rid) => Vue.reactive(new ResModel(api, rid))) | ||
.registerCollectionType(">", (api, rid) => Vue.reactive(new ResCollection(api, rid))); | ||
/* ... */ | ||
}, | ||
/* ... */ | ||
}); | ||
``` | ||
|
||
## Things to try out | ||
|
||
**Realtime updates** | ||
Run the client in two separate tabs to observe realtime updates. | ||
|
||
**System reset** | ||
Run the client and make some changes. Restart the server to observe resetting of resources in client. | ||
|
||
**Resynchronization** | ||
Run the client on two separate devices. Disconnect one device, then make changes with the other. Reconnect the first device to observe resynchronization. | ||
|
||
|
||
## Web resources | ||
|
||
### Get book collection | ||
``` | ||
GET http://localhost:8080/api/bookService/books | ||
``` | ||
|
||
### Get book | ||
``` | ||
GET http://localhost:8080/api/bookService/book/<BOOK_ID> | ||
``` | ||
|
||
### Update book properties | ||
``` | ||
POST http://localhost:8080/api/bookService/book/<BOOK_ID>/set | ||
``` | ||
*Body* | ||
``` | ||
{ "title": "Animal Farming" } | ||
``` | ||
|
||
### Add new book | ||
``` | ||
POST http://localhost:8080/api/bookService/books/add | ||
``` | ||
*Body* | ||
``` | ||
{ "title": "Dracula", "author": "Bram Stoker" } | ||
``` | ||
|
||
### Delete book | ||
``` | ||
POST http://localhost:8080/api/bookService/books/delete | ||
``` | ||
*Body* | ||
``` | ||
{ "id": <BOOK_ID> } | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
{ | ||
"name": "book-collection-vuejs", | ||
"description": "Resgate Book Collection Example for Vue 3", | ||
"main": "server.js", | ||
"dependencies": { | ||
"connect": "^3.6.6", | ||
"nats": "^1.0.1", | ||
"serve-static": "^1.13.2" | ||
}, | ||
"devDependencies": {}, | ||
"scripts": { | ||
"start": "node server.js" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
// Connect to NATS server | ||
const nats = require('nats').connect("nats://localhost:4222"); | ||
|
||
// Predefined responses | ||
const errorNotFound = JSON.stringify({ error: { code: "system.notFound", message: "Not found" }}); | ||
const accessGranted = JSON.stringify({ result: { get: true, call: "*" }}); | ||
const successResponse = JSON.stringify({ result: null }); | ||
const errorInvalidParams = (message) => JSON.stringify({ error: { code: "system.invalidParams", message }}); | ||
|
||
// Map of all book models | ||
let bookModels = { | ||
"library.book.1": { id: 1, title: "Animal Farm", author: "George Orwell" }, | ||
"library.book.2": { id: 2, title: "Brave New World", author: "Aldous Huxley" }, | ||
"library.book.3": { id: 3, title: "Coraline", author: "Neil Gaiman" } | ||
}; | ||
|
||
// Collection of books | ||
var books = [ | ||
{ rid: "library.book.1" }, | ||
{ rid: "library.book.2" }, | ||
{ rid: "library.book.3" } | ||
]; | ||
|
||
// ID counter for book models | ||
let nextBookID = books.length + 1; | ||
|
||
// Validation function | ||
function getError(field, value) { | ||
// Check we actually got a string | ||
if (typeof value !== "string") { | ||
return errorInvalidParams(field + " must be a string"); | ||
} | ||
// Check value is not empty | ||
if (!value.trim()) { | ||
return errorInvalidParams(field + " must not be empty"); | ||
} | ||
} | ||
|
||
// Access listener for all library resources. Everyone gets full access | ||
nats.subscribe('access.library.>', (req, reply) => { | ||
nats.publish(reply, accessGranted); | ||
}); | ||
|
||
// Book get listener | ||
nats.subscribe('get.library.book.*', function(req, reply, subj) { | ||
let rid = subj.substring(4); // Remove "get." to get resource ID | ||
let model = bookModels[rid]; | ||
if (model) { | ||
nats.publish(reply, JSON.stringify({ result: { model }})); | ||
} else { | ||
nats.publish(reply, errorNotFound); | ||
} | ||
}); | ||
|
||
// Book set listener | ||
nats.subscribe('call.library.book.*.set', (req, reply, subj) => { | ||
let rid = subj.substring(5, subj.length - 4); // Remove "call." and ".set" to get resource ID | ||
let model = bookModels[rid]; | ||
if (!model) { | ||
nats.publish(reply, errorNotFound); | ||
return; | ||
} | ||
|
||
let r = JSON.parse(req); | ||
let p = r.params || {}; | ||
let changed = null; | ||
|
||
// Check if title property was set | ||
if (p.title !== undefined) { | ||
let err = getError("Title", p.title); | ||
if (err) { | ||
nats.publish(reply, err); | ||
return; | ||
} | ||
changed = Object.assign({}, changed, { title: p.title }); | ||
} | ||
|
||
// Check if author property was set | ||
if (p.title !== undefined) { | ||
let err = getError("Author", p.author); | ||
if (err) { | ||
nats.publish(reply, err); | ||
return; | ||
} | ||
changed = Object.assign({}, changed, { author: p.author }); | ||
} | ||
|
||
// Publish update event on property changed | ||
if (changed) { | ||
nats.publish("event." + rid + ".change", JSON.stringify(changed)); | ||
} | ||
|
||
// Reply success by sending an empty result | ||
nats.publish(reply, successResponse); | ||
}); | ||
|
||
// Books get listener | ||
nats.subscribe('get.library.books', function(req, reply, subj) { | ||
nats.publish(reply, JSON.stringify({ result: { collection: books }})); | ||
}); | ||
|
||
// Books new listener | ||
nats.subscribe('call.library.books.new', (req, reply) => { | ||
let r = JSON.parse(req); | ||
let p = r.params || {}; | ||
|
||
let title = p.title || ""; | ||
let author = p.author || ""; | ||
|
||
// Check if title and author was set | ||
if (!title || !author) { | ||
nats.publish(reply, errorInvalidParams("Must provide both title and author")); | ||
return; | ||
} | ||
|
||
let id = nextBookID++; // Book ID | ||
let rid = "library.book." + id; // Book's resource ID | ||
let book = { id, title, author }; // Book's model | ||
let ref = { rid }; // Reference value to the book | ||
|
||
bookModels[rid] = book; | ||
// Publish add event | ||
nats.publish("event.library.books.add", JSON.stringify({ value: ref, idx: books.length })); | ||
books.push(ref); | ||
|
||
// Reply success by sending an empty result | ||
nats.publish(reply, successResponse); | ||
}); | ||
|
||
// Books delete listener | ||
nats.subscribe('call.library.books.delete', (req, reply) => { | ||
let r = JSON.parse(req); | ||
let p = r.params || {}; | ||
|
||
let id = p.id; // Book ID | ||
|
||
// Check if the book ID is a number | ||
if (typeof id !== "number") { | ||
nats.publish(reply, errorInvalidParams("Book ID must be a number")); | ||
return; | ||
} | ||
|
||
let rid = "library.book." + id; // Book's resource ID | ||
// Check if book exists | ||
if (bookModels[rid]) { | ||
// Delete the model and remove the reference from the collection | ||
delete bookModels[rid]; | ||
let idx = books.findIndex(v => v.rid === rid); | ||
if (idx >= 0) { | ||
books.splice(idx, 1); | ||
// Publish remove event | ||
nats.publish("event.library.books.remove", JSON.stringify({ idx })); | ||
} | ||
} | ||
|
||
// Reply success by sending an empty result | ||
nats.publish(reply, successResponse); | ||
}); | ||
|
||
// System resets tells Resgate that the service has been (re)started. | ||
// Resgate will then update any cached resource from library | ||
nats.publish('system.reset', JSON.stringify({ resources: [ 'library.>' ] })); | ||
|
||
|
||
// Run a simple webserver to serve the client. | ||
// This is only for the purpose of making the example easier to run | ||
var path = require('path'); | ||
const connect = require('connect'); | ||
const serveStatic = require('serve-static'); | ||
connect().use(serveStatic(path.join(__dirname, 'src'))).listen(8083, () => { | ||
console.log('Client available at http://localhost:8083'); | ||
}); |
Oops, something went wrong.