diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..4df1318
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,22 @@
+# EditorConfig helps developers define and maintain consistent
+# coding styles between different editors and IDEs
+# editorconfig.org
+
+root = true
+
+# Change these settings to your own preference
+[*]
+indent_style = space
+indent_size = 4
+tab_width = 4
+
+# Matches the exact files either package.json or .travis.yml
+[{package.json,.travis.yml}]
+indent_style = space
+indent_size = 2
+
+# We recommend you to keep these unchanged
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
diff --git a/.gitignore b/.gitignore
index 45d62d8..be65907 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,28 @@
+.idea
+
+/dev
+
*.sw?
+
+# Logs
+logs
+*.log
+
+# Runtime data
+pids
+*.pid
+*.seed
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directory
+# Commenting this out is preferred by some people, see
+# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
+node_modules
+
+# Users Environment Variables
+.lock-wscript
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..978d2f4
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,13 @@
+.*.swp
+._*
+.DS_Store
+.editorconfig
+.git
+.gitignore
+.hg
+.lock-wscript
+.wafpickle-*
+CVS
+npm-debug.log
+travis.yml
+/docs
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..89deefa
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,6 @@
+language: node_js
+node_js:
+ - "0.12"
+before_script:
+ - "sh -e /etc/init.d/xvfb start"
+ - npm install node-red-firebase
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..94dc9f9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 André L.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/README.md b/README.md
index 87d9945..037d185 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,66 @@
# Firebase nodes for Node-RED
-Check it out! Now you can access your Firebase data with Node-RED!
+[](https://gemnasium.com/vergissberlin/node-red-firebase) [](https://travis-ci.org/vergissberlin/node-red-firebase) [](http://inch-ci.org/github/vergissberlin/node-red-firebase) [](https://github.com/vergissberlin/node-red-firebase/issues "GitHub ticket system") [](https://npmjs.org/package/node-red-firebase "View this project on npm") [](https://gitter.im/vergissberlin/node-red-firebase?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://greenkeeper.io/)
+
+---
+
+Check it out! Now you can access your Firebase data with Node-RED!
This allows you to automate Firebase data manipulation or generate custom events based on what's going on with your data store.
-Installing node-red-firebase
-----------------------------
- npm install firebase
- cd nodes/
- git clone https://github.com/hovissimo/node-red-firebase
+  
+
+
+## Dedicated to
+
+**If you are using**
+
+- the fantastic program [Node-RED](http://nodered.org),
+- and you wanna interact with [Firebase API](https://www.firebase.com) [1](#glossary),
+
+**that node is made for you!**
+
+---
+
+## Features
-Check out the demo flows
------------------------
-To see the Firebase nodes in action, you can start Node-RED with
+1. **Modify node**
+ - [SET](https://www.firebase.com/docs/web/api/firebase/set.html), [PUSH](https://www.firebase.com/docs/web/api/firebase/push.html), [UPDATE](https://www.firebase.com/docs/web/api/firebase/update.html), [REMOVE](https://www.firebase.com/docs/web/api/firebase/remove.html) record
+2. **Watch node**
+ - Watch [on changes](https://www.firebase.com/docs/web/api/query/on.html) _Firebase_ API childs and send content to the output of the node
+3. **Query node**
+ - Query to a value of a Firebase node
- node red nodes/node-red-firebase/demo_flows.json
-
+## Installation
-Note: You'll need to register your own Firebase account, and edit all of the Firebase nodes to use your personal Firebase URL.
+ cd ~/.node-red
+ npm install node-red-firebase
+ node-red -v
+
+Open your *Node-RED* Frontend and you will find the new node under the group *output*. **Happy wiring!**
+
+## Demo
+Check out the demo flows to see the Firebase nodes in action, you can start Node-RED with
+
+ node-red node_modules/node-red-firebase/demo_flows.json
It's easiest to see what's going on if you have the live Firebase view open in another browser window while you interact with the flows.
-Have questions? Found a bug?
------------------------------
-Please submit issues to the Github issue tracker
+## Bugs, questions, contribute
+- **Found a bug?** Please submit issues to the [Github issue tracker](https://github.com/vergissberlin/node-red-firebase/issues).
+- **Have questions?** Please use [Gitter](https://gitter.im/vergissberlin/node-red-firebase?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) to get in contact with me
+- **Wanna contribute?** Please make a [fork](https://github.com/vergissberlin/node-red-firebase#fork-destination-box) an send me an pull request.
+
+
+## Thanks
+Special thanks to
+- Dave Conway-Jones and Nick O'Leary from IBM, founders of Node-RED.
+- [hvissimo](https://github.com/hovissimo) for base of the Firebase node.
+
+
+---
+
+**Glossary **
+
+1. *[PM2](https://github.com/Unitech/pm2) Firebase is a cloud services provider and backend as a service company based in San Francisco, California. The company makes a number of products for software developers building mobile or web applications.*
diff --git a/docs/node-modify-200.png b/docs/node-modify-200.png
new file mode 100644
index 0000000..8eb6837
Binary files /dev/null and b/docs/node-modify-200.png differ
diff --git a/docs/node-modify-300.png b/docs/node-modify-300.png
new file mode 100644
index 0000000..541e35f
Binary files /dev/null and b/docs/node-modify-300.png differ
diff --git a/docs/node-modify-500.png b/docs/node-modify-500.png
new file mode 100644
index 0000000..afe442d
Binary files /dev/null and b/docs/node-modify-500.png differ
diff --git a/docs/node-query-200.png b/docs/node-query-200.png
new file mode 100644
index 0000000..c4fce29
Binary files /dev/null and b/docs/node-query-200.png differ
diff --git a/docs/node-query-300.png b/docs/node-query-300.png
new file mode 100644
index 0000000..56acadb
Binary files /dev/null and b/docs/node-query-300.png differ
diff --git a/docs/node-query-500.png b/docs/node-query-500.png
new file mode 100644
index 0000000..aca2f5d
Binary files /dev/null and b/docs/node-query-500.png differ
diff --git a/docs/node-watch-200.png b/docs/node-watch-200.png
new file mode 100644
index 0000000..984f4a2
Binary files /dev/null and b/docs/node-watch-200.png differ
diff --git a/docs/node-watch-300.png b/docs/node-watch-300.png
new file mode 100644
index 0000000..3094e0b
Binary files /dev/null and b/docs/node-watch-300.png differ
diff --git a/docs/node-watch-500.png b/docs/node-watch-500.png
new file mode 100644
index 0000000..ca0b5a6
Binary files /dev/null and b/docs/node-watch-500.png differ
diff --git a/docs/settings-modify-500.png b/docs/settings-modify-500.png
new file mode 100644
index 0000000..e098782
Binary files /dev/null and b/docs/settings-modify-500.png differ
diff --git a/firebase_login.html b/firebase_login.html
new file mode 100644
index 0000000..a85459c
--- /dev/null
+++ b/firebase_login.html
@@ -0,0 +1,113 @@
+
+
+
diff --git a/firebase_login.js b/firebase_login.js
new file mode 100644
index 0000000..e16404e
--- /dev/null
+++ b/firebase_login.js
@@ -0,0 +1,144 @@
+module.exports = function (RED) {
+ 'use strict';
+
+ /**
+ * FirebaseLoginNode
+ *
+ * The Server Definition - this opens (and closes) the connection
+ *
+ * @param n
+ * @constructor
+ */
+ function FirebaseLoginNode(n) {
+
+ var Firebase,
+ node = this,
+ firebaseStatus = require('./utility/status');
+
+ RED.nodes.createNode(this, n);
+
+ this.appid = n.appid;
+ this.type = n.type;
+ this.uid = n.uid;
+ this.email = n.email;
+ this.password = n.password;
+ this.secret = n.secret;
+
+ Firebase = require('firebase');
+
+ // Retrieve the config node
+ this.server = RED.nodes.getNode(n.server);
+
+ if (this.credentials
+ && this.credentials.appid
+ && this.credentials.type) {
+
+ firebaseStatus.connecting(this);
+
+ this.url = 'https://' + this.credentials.appid + '.firebaseio.com';
+ global.refFirebase = new Firebase(this.url);
+ switch (this.credentials.type) {
+ case 'custom':
+ LoginTypeCustom(node, firebaseStatus);
+ break;
+ case 'email':
+ LoginTypeEmail(node);
+ break;
+ }
+ }
+ else {
+ this.error('Check your credentials! (Login node)');
+ }
+
+ }
+
+ /**
+ * Fire base login via user and secret
+ * @constructor
+ * @return void
+ */
+ function LoginTypeCustom(node) {
+ var FirebaseLoginCustom,
+ firebaseLoginCustom;
+
+ FirebaseLoginCustom = require('firebase-login-custom');
+
+ node.log('Firebase login custom');
+
+ if (node.credentials.secret &&
+ node.credentials.uid) {
+ node.data = {
+ uid: node.credentials.uid,
+ secret: node.credentials.secret
+ };
+
+ firebaseLoginCustom = new FirebaseLoginCustom(node.refFirebase,
+ {
+ uid: node.data.uid
+ },
+ {
+ secret: node.data.secret
+ },
+ function (error) {
+ if (error !== null) {
+ node.error('Login error with custom login');
+ node.error(error);
+ } else {
+ node.log('Login successful');
+ }
+ }
+ );
+ } else {
+ node.error('Check your secret!');
+ }
+ }
+
+ /**
+ * Fire base login via password and email
+ * @constructor
+ * @return void
+ */
+ function LoginTypeEmail(node) {
+ var FirebaseLoginEmail,
+ firebaseLoginEmail;
+
+ FirebaseLoginEmail = require('firebase-login-email');
+
+ node.log('Firebase login email');
+
+ if (node.credentials.email &&
+ node.credentials.password) {
+
+ node.data = {
+ email: node.credentials.email,
+ password: node.credentials.password
+ };
+
+ firebaseLoginEmail = new FirebaseLoginEmail(
+ global.refFirebase,
+ node.data,
+ function (error) {
+ if (error !== null) {
+ node.error('Login error with custom login');
+ node.error(error);
+ } else {
+ node.log('Login successful');
+ }
+ }
+ );
+ } else {
+ node.error('Check your email credentials!');
+ }
+ }
+
+ RED.nodes.registerType('firebase login', FirebaseLoginNode, {
+ credentials: {
+ appid: {type: 'text'},
+ type: {type: 'text'},
+ uid: {type: 'text'},
+ email: {type: 'text'},
+ password: {type: 'password'},
+ secret: {type: 'text'}
+ }
+ });
+};
diff --git a/firebase_modify.html b/firebase_modify.html
index de7ca04..e948cbe 100644
--- a/firebase_modify.html
+++ b/firebase_modify.html
@@ -1,7 +1,12 @@
diff --git a/firebase_modify.js b/firebase_modify.js
index 843cf5e..7bf1894 100644
--- a/firebase_modify.js
+++ b/firebase_modify.js
@@ -1,40 +1,59 @@
-module.exports = function(RED) {
- "use strict";
- var Firebase = require("firebase");
+module.exports = function (RED) {
+ 'use strict';
function FirebaseModify(n) {
- RED.nodes.createNode(this,n);
- this.firebaseurl = n.firebaseurl;
+ var Firebase = require('firebase'),
+ firebaseStatus = require('./utility/status');
+
+ RED.nodes.createNode(this, n);
+
this.child = n.child;
+ this.credentials = RED.nodes.getNode(n.firebaselogin).credentials;
+ this.firebasepath = n.firebasepath;
this.method = n.method;
- this.firebase = new Firebase(this.firebaseurl);
-
- switch (this.method) {
- case "set":
- case "update":
- case "push":
- // To prevent code repetition, call the Firebase API function based on method directly
- this.on('input', function(msg) {
- // get path from msg or default to /
- var childpath = (this.child) ? msg[this.child] : "";
- // make sure the path starts with /
- childpath = (childpath.indexOf("/") == 0) ? childpath : "/" + childpath;
-
- this.firebase.child(childpath)[this.method](msg.payload);
- });
- break;
- case "remove":
- // Remove method expects first argument to be a function, so we call it differently
- this.on('input', function(msg) {
- // get path from msg or default to /
- var childpath = (this.child) ? msg[this.child] : "";
- // make sure the path starts with /
- childpath = (childpath.indexOf("/") == 0) ? childpath : "/" + childpath;
-
- this.firebase.child(childpath)[this.method]();
- });
- break;
+
+ // Status
+ firebaseStatus.connecting(this);
+
+ // Retrieve the config node
+ if (!this.credentials.appid) {
+ firebaseStatus.error(this, 'Check credentials!');
+ this.error('You need to setup Firebase credentials!');
+ } else {
+ this.firebaseurl = 'https://' + this.credentials.appid + '.firebaseio.com/' + this.firebasepath;
+ this.firebase = new Firebase(this.firebaseurl);
+
+ // Status
+ firebaseStatus.checkStatus(this);
+
+ switch (this.method) {
+ case 'set':
+ case 'update':
+ case 'push':
+ // To prevent code repetition, call the Firebase API function based on method directly
+ this.on('input', function (msg) {
+ // get path from msg or default to /
+ var childpath = (this.child) ? msg[this.child] : '';
+ // make sure the path starts with /
+ childpath = (childpath.indexOf('/') == 0) ? childpath : '/' + childpath;
+
+ this.firebase.child(childpath)[this.method](msg.payload);
+ });
+ break;
+ case 'remove':
+ // Remove method expects first argument to be a function, so we call it differently
+ this.on('input', function (msg) {
+ // get path from msg or default to /
+ var childpath = (this.child) ? msg[this.child] : '';
+ // make sure the path starts with /
+ childpath = (childpath.indexOf('/') == 0) ? childpath : '/' + childpath;
+
+ this.firebase.child(childpath)[this.method]();
+ });
+ break;
+ }
}
}
- RED.nodes.registerType("firebase modify", FirebaseModify);
-}
+
+ RED.nodes.registerType('firebase modify', FirebaseModify);
+};
diff --git a/firebase_query.html b/firebase_query.html
index c88bf0a..a3e2599 100644
--- a/firebase_query.html
+++ b/firebase_query.html
@@ -1,7 +1,12 @@
diff --git a/firebase_query.js b/firebase_query.js
index 4b0c509..36ff40a 100644
--- a/firebase_query.js
+++ b/firebase_query.js
@@ -1,25 +1,43 @@
-module.exports = function(RED) {
- "use strict";
- var Firebase = require("firebase");
+module.exports = function (RED) {
+ 'use strict';
function sendMessageFromSnapshot(msg, snapshot) {
msg.href = snapshot.ref().toString();
msg.payload = snapshot.val();
this.send(msg);
}
+
function FirebaseQuery(n) {
- RED.nodes.createNode(this,n);
+ var Firebase = require('firebase'),
+ firebaseStatus = require('./utility/status');
+
+ RED.nodes.createNode(this, n);
- this.firebaseurl = n.firebaseurl;
this.child = n.child;
- this.firebase = new Firebase(this.firebaseurl);
+ this.credentials = RED.nodes.getNode(n.firebaselogin).credentials;
+ this.firebasepath = n.firebasepath;
+
+ // Status
+ firebaseStatus.connecting(this);
- this.on('input', function(msg) {
- var childpath = (this.child) ? String(msg[this.child]) : ""; // get path from msg or default to /
- childpath = (childpath.indexOf("/") == 0) ? childpath : "/" + childpath; // make sure the path starts with /
+ // Check credentials
+ if (!this.credentials.appid) {
+ firebaseStatus.error(this, 'Check credentials! (Query)');
+ } else {
+ this.firebaseurl = 'https://' + this.credentials.appid + '.firebaseio.com/' + this.firebasepath;
+ this.firebase = new Firebase(this.firebaseurl);
- this.firebase.child(childpath).once('value', sendMessageFromSnapshot.bind(this, msg));
- });
+ // Status
+ firebaseStatus.checkStatus(this);
+
+ // Add listener
+ this.on('input', function (msg) {
+ var childpath = (this.child) ? String(msg[this.child]) : ''; // get path from msg or default to
+ childpath = (childpath.indexOf('/') == 0) ? childpath : '/' + childpath; // make sure the path starts with
+ this.firebase.child(childpath).once('value', sendMessageFromSnapshot.bind(this, msg));
+ });
+ }
}
- RED.nodes.registerType("firebase query", FirebaseQuery);
-}
+
+ RED.nodes.registerType('firebase query', FirebaseQuery);
+};
diff --git a/firebase_watch.html b/firebase_watch.html
index ba80a0f..9057325 100644
--- a/firebase_watch.html
+++ b/firebase_watch.html
@@ -1,14 +1,13 @@
diff --git a/firebase_watch.js b/firebase_watch.js
index 749b018..da2b4c6 100644
--- a/firebase_watch.js
+++ b/firebase_watch.js
@@ -1,6 +1,5 @@
module.exports = function(RED) {
- "use strict";
- var Firebase = require("firebase");
+ 'use strict';
function sendMessageFromSnapshot(snapshot) {
var msg = {};
@@ -10,18 +9,35 @@ module.exports = function(RED) {
}
function FirebaseWatch(n) {
+ var Firebase = require('firebase'),
+ firebaseStatus = require('./utility/status');
+
RED.nodes.createNode(this,n);
- this.firebaseurl = n.firebaseurl;
- this.firebase = new Firebase(this.firebaseurl);
+ this.credentials = RED.nodes.getNode(n.firebaselogin).credentials;
this.onValue = sendMessageFromSnapshot.bind(this);
+ this.firebasepath = n.firebasepath;
+
+ // Status
+ firebaseStatus.connecting(this);
+
+ // Check credentials
+ if (!this.credentials.appid) {
+ firebaseStatus.error(this,'Check credentials!');
+ this.error('You need to setup Firebase credentials!');
+ } else {
+ this.firebaseurl = 'https://' + this.credentials.appid + '.firebaseio.com/' + this.firebasepath;
+ this.firebase = new Firebase(this.firebaseurl);
- this.firebase.on("value", this.onValue);
+ // Status
+ firebaseStatus.checkStatus(this);
- this.on('close', function() {
- // We need to unbind our callback, or we'll get duplicate messages when we redeploy
- this.firebase.off("value", this.onValue);
- });
+ this.firebase.on('value', this.onValue);
+ this.on('close', function() {
+ // We need to unbind our callback, or we'll get duplicate messages when we redeploy
+ this.firebase.off('value', this.onValue);
+ });
+ }
}
- RED.nodes.registerType("firebase watch", FirebaseWatch);
-}
+ RED.nodes.registerType('firebase watch', FirebaseWatch);
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..85fe35c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "node-red-firebase",
+ "version": "0.1.2",
+ "description": "Check it out! Now you can access your Firebase data with Node-RED! This allows you to automate Firebase data manipulation or generate custom events based on what's going on with your data store.",
+ "main": "firebase_query.js",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/vergissberlin/node-red-firebase.git"
+ },
+ "scripts": {
+ "test": "npm install"
+ },
+ "keywords": [
+ "node-red",
+ "node",
+ "firebase",
+ "real",
+ "time",
+ "socket"
+ ],
+ "node-red": {
+ "nodes": {
+ "firebase_modify": "firebase_modify.js",
+ "firebase_query": "firebase_query.js",
+ "firebase_watch": "firebase_watch.js"
+ }
+ },
+ "author": "André Lademann ",
+ "contributors": [
+ "André Lademann ",
+ "Hovis "
+ ],
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/vergissberlin/node-red-firebase/issues"
+ },
+ "homepage": "https://github.com/vergissberlin/node-red-firebase",
+ "dependencies": {
+ "firebase": "^5.1.0",
+ "firebase-login": "^1.0.0",
+ "firebase-login-custom": "0.0.4",
+ "firebase-login-email": "^0.0.4"
+ }
+}
diff --git a/utility/status.js b/utility/status.js
new file mode 100644
index 0000000..c276cbe
--- /dev/null
+++ b/utility/status.js
@@ -0,0 +1,124 @@
+/**
+ * Status
+ *
+ * @type {{offline: Function, connecting: Function, connected: Function, error: Function, addListener: Function}}
+ */
+module.exports = {
+
+ /**
+ * Status offline
+ *
+ * @param node
+ */
+ offline: function (node) {
+ node.status({
+ fill: 'gray',
+ shape: 'ring',
+ text: 'disconnected'
+ }
+ );
+ },
+
+ /**
+ * Status connecting
+ *
+ * @param node
+ */
+ connecting: function (node) {
+ node.status({
+ fill: 'grey',
+ shape: 'ring',
+ text: 'connecting'
+ }
+ );
+ },
+
+ /**
+ * Status connected
+ *
+ * @param node
+ */
+ connected: function (node) {
+ node.status({
+ fill: 'green',
+ shape: 'dot',
+ text: 'connected'
+ }
+ );
+ },
+
+ /**
+ * Status error
+ *
+ * @param node
+ * @param msg
+ */
+ error: function (node, msg) {
+ node.status({
+ fill: 'red',
+ shape: 'ring',
+ text: msg || 'connection failed'
+ }
+ );
+ },
+
+ /**
+ * AddEventListener
+ *
+ * @param node
+ */
+ addListener: function (node) {
+
+ var self = this;
+
+ global.refFirebase.onAuth(function(authData) {
+ if (authData) {
+ self.connected(node);
+ } else {
+ self.error(node);
+ }
+ });
+
+ global.refFirebase.offAuth(function(authData) {
+ if (authData) {
+ self.connected(node);
+ } else {
+ self.offline(node);
+ }
+ });
+ },
+
+ /**
+ * AddEventListener
+ *
+ * @param node
+ */
+ checkStatus: function (node) {
+
+ var authData,
+ self = this;
+
+ node.firebase.onAuth(function(authData) {
+ if (authData) {
+ self.connected(node);
+ } else {
+ self.error(node);
+ }
+ });
+
+ node.firebase.offAuth(function(authData) {
+ if (authData) {
+ self.connected(node);
+ } else {
+ self.offline(node);
+ }
+ });
+
+ authData = node.firebase.getAuth();
+ if (authData) {
+ self.connected(node);
+ } else {
+ self.offline(node);
+ }
+ }
+};