Skip to content

Commit

Permalink
Merge babefda into 7144c8a
Browse files Browse the repository at this point in the history
  • Loading branch information
jirenius committed May 24, 2021
2 parents 7144c8a + babefda commit 076a27e
Show file tree
Hide file tree
Showing 9 changed files with 554 additions and 6 deletions.
15 changes: 11 additions & 4 deletions docs/docs.md
Expand Up @@ -40,9 +40,9 @@ ResClient represents a client connection to a RES API.
* [.on(events, handler)](#ResClient+on)
* [.off(events, [handler])](#ResClient+off)
* [.setOnConnect(onConnect)](#ResClient+setOnConnect) ⇒ <code>this</code>
* [.registerModelType(pattern, factory)](#ResClient+registerModelType)
* [.registerModelType(pattern, factory)](#ResClient+registerModelType) ⇒ <code>this</code>
* [.unregisterModelType(pattern)](#ResClient+unregisterModelType)[<code>modelFactory</code>](#ResClient..modelFactory)
* [.registerCollectionType(pattern, factory)](#ResClient+registerCollectionType)
* [.registerCollectionType(pattern, factory)](#ResClient+registerCollectionType) ⇒ <code>this</code>
* [.unregisterCollectionType(pattern)](#ResClient+unregisterCollectionType)[<code>collectionFactory</code>](#ResClient..collectionFactory)
* [.get(rid, [collectionFactory])](#ResClient+get) ⇒ <code>Promise.&lt;(ResModel\|ResCollection)&gt;</code>
* [.call(rid, method, params)](#ResClient+call) ⇒ <code>Promise.&lt;object&gt;</code>
Expand Down Expand Up @@ -140,7 +140,7 @@ Sets the onConnect callback.

<a name="ResClient+registerModelType"></a>

### resClient.registerModelType(pattern, factory)
### resClient.registerModelType(pattern, factory) ⇒ <code>this</code>
Register a model type.
The pattern may use the following wild cards:
* The asterisk (*) matches any part at any level of the resource name.
Expand All @@ -167,7 +167,7 @@ Unregister a previously registered model type pattern.

<a name="ResClient+registerCollectionType"></a>

### resClient.registerCollectionType(pattern, factory)
### resClient.registerCollectionType(pattern, factory) ⇒ <code>this</code>
Register a collection type.
The pattern may use the following wild cards:
* The asterisk (*) matches any part at any level of the resource name.
Expand Down Expand Up @@ -352,6 +352,7 @@ ResCollection represents a collection provided over the RES API.
* [new ResCollection(api, rid, [opt])](#new_ResCollection_new)
* _instance_
* [.length](#ResCollection+length)
* [.list](#ResCollection+list)
* [.getClient()](#ResCollection+getClient)[<code>ResClient</code>](#ResClient)
* [.getResourceId()](#ResCollection+getResourceId) ⇒ <code>string</code>
* [.on([events], [handler])](#ResCollection+on) ⇒ <code>this</code>
Expand Down Expand Up @@ -386,6 +387,12 @@ Creates an ResCollection instance
### resCollection.length
Length of the collection

**Kind**: instance property of [<code>ResCollection</code>](#ResCollection)
<a name="ResCollection+list"></a>

### resCollection.list
Internal collection array. Do not mutate directly.

**Kind**: instance property of [<code>ResCollection</code>](#ResCollection)
<a name="ResCollection+getClient"></a>

Expand Down
95 changes: 95 additions & 0 deletions examples/book-collection-vue3/README.md
@@ -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> }
```
14 changes: 14 additions & 0 deletions examples/book-collection-vue3/package.json
@@ -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"
}
}
172 changes: 172 additions & 0 deletions examples/book-collection-vue3/server.js
@@ -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');
});

0 comments on commit 076a27e

Please sign in to comment.