├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── lib └── index.js ├── package.json ├── samples ├── chat │ ├── README.md │ ├── app.js │ ├── authentication.js │ ├── index.js │ ├── models.js │ └── public │ │ ├── app.js │ │ └── index.html └── posts │ ├── README.md │ ├── index.js │ └── models.js ├── src └── index.js └── test └── index.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "node": 6 8 | } 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true, 7 | "jest": true 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 8, 11 | "ecmaFeatures": { 12 | "jsx": true 13 | }, 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-const-assign": "warn", 18 | "no-this-before-super": "warn", 19 | "no-undef": "error", 20 | "no-unreachable": "warn", 21 | "no-unused-vars": "warn", 22 | "constructor-super": "warn", 23 | "valid-typeof": "warn" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | data 4 | npm-debug 5 | lib 6 | package-lock.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 7 4 | - 6 5 | - 4 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Giap Nguyen Huu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nextql-feathers 2 | NextQL plugin for feathers. I not sure it for production, but it demonstrate how easy to extend NextQL 3 | 4 | [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url] 5 | 6 | * [NextQL](https://github.com/giapnguyen74/nextql) : Yet Another Data Query Language. Equivalent GraphQL but much more simple. 7 | * [Featherjs](https://github.com/feathersjs/feathers) : A REST and realtime API layer for modern applications. 8 | 9 | > Notice: Current nextql-feathers only work with nextql >= 5.0.0 10 | 11 | # Install 12 | ```sh 13 | npm install --save nextql-feathers 14 | ``` 15 | 16 | # Why ? 17 | NextQL just a data query engine. It required a client-side component, a transport and a data access component to complete. Featherjs just happen provide all of features. So shall we marry? 18 | 19 | In fact, NextQL match perfect with Feathers: 20 | * NextQL use JS object as model, Feathers use JS object as service. 21 | * NextQL's methods could map to Feathers methods. 22 | * Finally, NextQL will complete Feathers with robust data/relationship query language. 23 | 24 | # Sample 25 | * [Nested services](https://github.com/giapnguyen74/nextql-feathers/tree/master/samples/posts) 26 | * [Featherjs chat](https://github.com/giapnguyen74/nextql-feathers/tree/master/samples/chat) 27 | 28 | # nextql + feathers = Awesome! 29 | 30 | ```js 31 | const NextQL = require("nextql"); 32 | const nextql = new NextQL(); 33 | 34 | 35 | const feathers = require("feathers"); 36 | const app = feathers(); 37 | const NeDB = require("nedb"); 38 | const service = require("feathers-nedb"); 39 | 40 | // Create a NeDB instance 41 | const Model = new NeDB({ 42 | filename: "./data/messages.db", 43 | autoload: true 44 | }); 45 | 46 | // Use nextql-feathers plugin 47 | nextql.use(require("nextql-feathers"), { 48 | app 49 | }); 50 | 51 | // Define NextQL model which also a feathers service 52 | nextql.model("messages", { 53 | feathers: { 54 | path: "/messages", 55 | service: service({ Model }) 56 | }, 57 | fields: { 58 | _id: 1, 59 | text: 1, 60 | newText: 1 61 | }, 62 | computed: { 63 | owner() { 64 | return { 65 | name: "Giap Nguyen Huu" 66 | }; 67 | } 68 | } 69 | }); 70 | 71 | 72 | // Now NextQL work seamlessly with Feathers 73 | await app.service("messages").find({ 74 | query: { 75 | $params: { $limit: 2 }, // featherjs find params 76 | _id: 1, 77 | text: 1, 78 | owner: { 79 | name: 1 // !!! NextQL resolve computed value for featherjs 80 | } 81 | } 82 | }) 83 | 84 | await app.service("messages").get(1, { 85 | query: { 86 | _id: 1, 87 | text: 1, 88 | owner: { 89 | name: 1 90 | } 91 | } 92 | }); 93 | 94 | 95 | await app.service("messages").patch( 96 | 2, 97 | { 98 | newText: "Text 2" 99 | }, 100 | { 101 | query: {} 102 | } 103 | ); 104 | ``` 105 | 106 | # Features 107 | Please check out [featherjs-chat](https://github.com/giapnguyen74/nextql-feathers/tree/master/samples/chat) example with NextQL. 108 | * NextQL could real-time over Featherjs socket.io 109 | * Featherjs methods called with NextQL query. So you can query user information directly from messages service. Orginial version require you query addtional user service. 110 | ```js 111 | client 112 | .service("messages") 113 | .find({ 114 | query: { 115 | $params: { 116 | $sort: { createdAt: -1 }, 117 | $limit: 25 118 | }, 119 | total: 1, 120 | limit: 1, 121 | skip: 1, 122 | data: { 123 | text: 1, 124 | owner: { 125 | name: 1 126 | } 127 | } 128 | } 129 | }) 130 | .then(page => { 131 | page.data.reverse().forEach(addMessage); 132 | }); 133 | ``` 134 | 135 | * When you call create message, you provide NextQL query which filter data from Featherjs event - **it's work like GraphQL subscription or Relay Fat Query without coding**. 136 | ```js 137 | client 138 | .service("messages") 139 | .create( 140 | { 141 | text: input.value 142 | }, 143 | { 144 | query: { 145 | text: 1, 146 | owner: { 147 | name: 1 148 | } 149 | } 150 | } 151 | ) 152 | .then(() => { 153 | input.value = ""; 154 | }); 155 | ``` 156 | 157 | * Thus event listenning from all clients will receive above NextQL query data. 158 | ```js 159 | // Listen to created events and add the new message in real-time 160 | client.service("messages").on("created", addMessage); 161 | ``` 162 | 163 | 164 | # Testing 165 | ``` 166 | PASS test/index.test.js 167 | ✓ find messages (8ms) 168 | ✓ get message (5ms) 169 | ✓ create message (2ms) 170 | ✓ update message (5ms) 171 | ✓ patch message (2ms) 172 | ✓ remove message (3ms) 173 | 174 | Test Suites: 1 passed, 1 total 175 | Tests: 6 passed, 6 total 176 | Snapshots: 0 total 177 | Time: 0.908s, estimated 2s 178 | Ran all test suites. 179 | 180 | File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines | 181 | ----------|----------|----------|----------|----------|----------------| 182 | All files | 97.92 | 89.29 | 100 | 97.87 | | 183 | index.js | 97.92 | 89.29 | 100 | 97.87 | 121 | 184 | 185 | ``` 186 | 187 | 188 | [npm-image]: https://badge.fury.io/js/nextql-feath.svg 189 | [npm-url]: https://npmjs.org/package/nextql-feathers 190 | [travis-image]: https://travis-ci.org/giapnguyen74/nextql-feathers.svg?branch=master 191 | [travis-url]: https://travis-ci.org/giapnguyen74/nextql-feathers 192 | [daviddm-image]: https://david-dm.org/giapnguyen74/nextql-feathers.svg?theme=shields.io 193 | [daviddm-url]: https://david-dm.org/giapnguyen74/nextql-feathers -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @example Extend nextql model with featherjs option 5 | * // { 6 | * // feathers: 'path' 7 | * // } 8 | */ 9 | 10 | class Service { 11 | constructor(options) { 12 | const name = options.name, 13 | nextql = options.nextql; 14 | 15 | 16 | this.name = name; 17 | this.nextql = nextql; 18 | } 19 | 20 | _execute(method, query, params) { 21 | const q = {}; 22 | q[this.name] = {}; 23 | q[this.name][method] = query; 24 | return this.nextql.execute(q, params) //featherjs params as context 25 | .then(result => result && result[this.name] && result[this.name][method] || null); 26 | } 27 | 28 | find(params) { 29 | const query = Object.assign({}, params && params.query); 30 | return this._execute("find", query, params); 31 | } 32 | 33 | get(id, params) { 34 | const query = Object.assign({}, params && params.query); 35 | query.$params = { id }; 36 | return this._execute("get", query, params); 37 | } 38 | 39 | create(data, params) { 40 | const query = Object.assign({}, params && params.query); 41 | query.$params = { data }; 42 | return this._execute("create", query, params); 43 | } 44 | 45 | update(id, data, params) { 46 | const query = Object.assign({}, params && params.query); 47 | query.$params = { id, data }; 48 | return this._execute("update", query, params); 49 | } 50 | 51 | patch(id, data, params) { 52 | const query = Object.assign({}, params && params.query); 53 | query.$params = { id, data }; 54 | return this._execute("patch", query, params); 55 | } 56 | 57 | remove(id, params) { 58 | const query = Object.assign({}, params && params.query); 59 | query.$params = { id }; 60 | return this._execute("remove", query, params); 61 | } 62 | } 63 | 64 | function inject_feather_methods(name, options, service) { 65 | options.returns = Object.assign({ 66 | get: name, 67 | create: name, 68 | update: name, 69 | patch: name, 70 | remove: name, 71 | find() { 72 | if (service.paginate && service.paginate.default) { 73 | return { 74 | total: 1, 75 | limit: 1, 76 | skip: 1, 77 | data: name 78 | }; 79 | } else { 80 | return name; 81 | } 82 | } 83 | }, options.returns); 84 | options.methods = Object.assign({ 85 | find(params, ctx) { 86 | return service.find({ 87 | query: ctx.query.$params 88 | }); 89 | }, 90 | 91 | get(params, ctx) { 92 | return service.get(params.id, ctx); 93 | }, 94 | 95 | create(params, ctx) { 96 | return service.create(params.data, ctx); 97 | }, 98 | 99 | update(params, ctx) { 100 | return service.update(params.id, params.data, ctx); 101 | }, 102 | 103 | patch(params, ctx) { 104 | return service.patch(params.id, params.data, ctx); 105 | }, 106 | 107 | remove(params, ctx) { 108 | return service.remove(params.id, ctx); 109 | } 110 | }, options.methods); 111 | } 112 | 113 | module.exports = { 114 | install(nextql, options) { 115 | const app = options && options.app; 116 | if (!app) { 117 | throw new Error("Missing feathers app object"); 118 | } 119 | 120 | nextql.beforeCreate(options => { 121 | const name = options.name; 122 | if (options.feathers) { 123 | var _options$feathers = options.feathers; 124 | const path = _options$feathers.path, 125 | service = _options$feathers.service; 126 | 127 | app.use(path, new Service({ 128 | name: options.name, 129 | nextql 130 | })); 131 | 132 | if (service) { 133 | inject_feather_methods(name, options, service); 134 | } 135 | } 136 | }); 137 | } 138 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextql-feathers", 3 | "version": "0.0.1", 4 | "description": "nextql plugin for feathers", 5 | "homepage": "", 6 | "author": "“Giap <“sorite2003@gmail.com”> (“https://www.linkedin.com/in/giapnh”)", 7 | "files": [ 8 | "lib" 9 | ], 10 | "main": "index.js", 11 | "keywords": [ 12 | "nextql", 13 | "feathers" 14 | ], 15 | "devDependencies": { 16 | "babel-cli": "^6.24.1", 17 | "babel-jest": "^20.0.3", 18 | "babel-preset-env": "^1.6.0", 19 | "body-parser": "^1.17.2", 20 | "eslint": "^4.4.1", 21 | "eslint-config-xo-space": "^0.16.0", 22 | "featherjs": "^0.1.0", 23 | "feathers": "^2.1.7", 24 | "feathers-authentication": "^1.2.7", 25 | "feathers-authentication-jwt": "^0.3.2", 26 | "feathers-authentication-local": "^0.4.3", 27 | "feathers-nedb": "^2.6.2", 28 | "feathers-rest": "^1.8.0", 29 | "feathers-socketio": "^2.0.0", 30 | "jest": "^20.0.4", 31 | "jest-cli": "^20.0.0", 32 | "nedb": "^1.8.0", 33 | "nsp": "^2.6.3" 34 | }, 35 | "scripts": { 36 | "test": "jest --coverage", 37 | "compile": "babel -d lib/ src/", 38 | "prepublish": "nsp check && npm run compile" 39 | }, 40 | "license": "MIT", 41 | "repository": "giapnguyen74@gmail.com/nextql-feathers", 42 | "jest": { 43 | "testEnvironment": "node" 44 | }, 45 | "dependencies": {} 46 | } 47 | -------------------------------------------------------------------------------- /samples/chat/README.md: -------------------------------------------------------------------------------- 1 | # Featherjs chat with NextQL -------------------------------------------------------------------------------- /samples/chat/app.js: -------------------------------------------------------------------------------- 1 | const NextQL = require("../../../nextql"); 2 | const nextql = new NextQL(); 3 | const models = require("./models"); 4 | module.exports = function() { 5 | nextql.use(require("../../src"), { 6 | app: this 7 | }); 8 | 9 | Object.keys(models).forEach(k => nextql.model(k, models[k])); 10 | }; 11 | -------------------------------------------------------------------------------- /samples/chat/authentication.js: -------------------------------------------------------------------------------- 1 | const authentication = require("feathers-authentication"); 2 | const jwt = require("feathers-authentication-jwt"); 3 | const local = require("feathers-authentication-local"); 4 | 5 | module.exports = function() { 6 | const app = this; 7 | const config = { 8 | secret: 9 | "220cac0f7d60fb3b50d5e53844076334d3143894836b1c1eb2c4c893ab6454da9da1a8d9147b94068080f0b25dd1d77d896fbb087cb056b815076ee6bd69c62a429d7f412fae52593bd66c0bc1a1b04dcafc37357072a8f26382d734ca830cc44e32c50852feffdd4691229b851a6a08bb1b4a7a14234c813fbb459ff9d519101786caa871d45073da38cf012a2120d0f5cd0f6c4fe33c4ceb383e8c306d46eae714ca96226637763a2f8621e81ccbf0d6f67b0b9306309c87f600043aa1d608fab7073f384a34615fbe7671aa550c869353d342eef9f34914886e44e58b1205f7ce7fff62bf73a8a2bd2451d5e99a09db877073462cf4f24495e5f65ad740c2", 10 | strategies: ["jwt", "local"], 11 | path: "/authentication", 12 | service: "users", 13 | jwt: { 14 | header: { 15 | type: "access" 16 | }, 17 | audience: "https://yourdomain.com", 18 | subject: "anonymous", 19 | issuer: "feathers", 20 | algorithm: "HS256", 21 | expiresIn: "1d" 22 | }, 23 | local: { 24 | entity: "user", 25 | service: "users", 26 | usernameField: "email", 27 | passwordField: "password" 28 | } 29 | }; 30 | 31 | // Set up authentication with the secret 32 | app.configure(authentication(config)); 33 | //app.configure(jwt()); 34 | app.configure(local(config.local)); 35 | 36 | // The `authentication` service is used to create a JWT. 37 | // The before `create` hook registers strategies that can be used 38 | // to create a new valid JWT (e.g. local or oauth2) 39 | app.service("authentication"); 40 | }; 41 | -------------------------------------------------------------------------------- /samples/chat/index.js: -------------------------------------------------------------------------------- 1 | const feathers = require("feathers"); 2 | const bodyParser = require("body-parser"); 3 | const rest = require("feathers-rest"); 4 | const socketio = require("feathers-socketio"); 5 | const app = feathers(); 6 | const path = require("path"); 7 | // Turn on JSON parser for REST services 8 | app.use(bodyParser.json()); 9 | // Turn on URL-encoded parser for REST services 10 | app.use(bodyParser.urlencoded({ extended: true })); 11 | 12 | app.use("/", feathers.static(path.join(__dirname, "public"))); 13 | 14 | // Set up REST transport 15 | app.configure(rest()); 16 | app.configure(socketio()); 17 | app.configure(require("./app")); 18 | 19 | app.listen(3000, function() { 20 | console.log("chat server@3000"); 21 | }); 22 | -------------------------------------------------------------------------------- /samples/chat/models.js: -------------------------------------------------------------------------------- 1 | const NeDB = require("nedb"); 2 | const service = require("feathers-nedb"); 3 | 4 | // Create a NeDB instance 5 | const Messages = new NeDB({ 6 | filename: "./data/messages.db", 7 | autoload: true 8 | }); 9 | 10 | // Create a NeDB instance 11 | const Users = new NeDB({ 12 | filename: "./data/users.db", 13 | autoload: true 14 | }); 15 | 16 | module.exports = { 17 | users: { 18 | feathers: { 19 | path: "/users", 20 | service: service({ Model: Users }) 21 | }, 22 | fields: { 23 | _id: 1, 24 | text: 1 25 | }, 26 | computed: {} 27 | }, 28 | messages: { 29 | feathers: { 30 | path: "/messages", 31 | service: service({ 32 | Model: Messages, 33 | paginate: { 34 | default: 10, 35 | max: 10 36 | } 37 | }) 38 | }, 39 | fields: { 40 | _id: 1, 41 | text: 1, 42 | newText: 1, 43 | owner: { 44 | name: 1 45 | } 46 | }, 47 | computed: { 48 | owner() { 49 | return { 50 | name: "Giap Nguyen Huu" 51 | }; 52 | } 53 | } 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /samples/chat/public/app.js: -------------------------------------------------------------------------------- 1 | // Establish a Socket.io connection 2 | const socket = io(); 3 | // Initialize our Feathers client application through Socket.io 4 | // with hooks and authentication. 5 | const client = feathers(); 6 | 7 | client.configure(feathers.socketio(socket)); 8 | 9 | // Login screen 10 | const loginHTML = `
11 |
12 |
13 |

Log in or signup

14 |
15 |
16 |
17 |
18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 | 28 | 31 |
32 |
33 |
34 |
`; 35 | 36 | // Chat base HTML (without user list and messages) 37 | const chatHTML = `
38 |
39 |
40 | 42 | Chat 43 |
44 |
45 |
46 | 59 |
60 |
61 |
62 | 63 | 64 |
65 |
66 |
67 |
`; 68 | 69 | // Add a new user to the list 70 | function addUser(user) { 71 | // Add the user to the list 72 | document.querySelector(".user-list").insertAdjacentHTML( 73 | "beforeend", 74 | `
  • 75 | 76 | 77 | ${user.email} 78 | 79 |
  • ` 80 | ); 81 | 82 | // Update the number of users 83 | document.querySelector( 84 | ".online-count" 85 | ).innerHTML = document.querySelectorAll(".user-list li").length; 86 | } 87 | 88 | // Renders a new message and finds the user that belongs to the message 89 | function addMessage(message) { 90 | console.log(message); 91 | // Find the user belonging to this message or use the anonymous user if not found 92 | const sender = message.owner || {}; 93 | const chat = document.querySelector(".chat"); 94 | 95 | chat.insertAdjacentHTML( 96 | "beforeend", 97 | `
    98 |
    99 |

    100 | ${sender.name} 101 | ${moment(message.createdAt).format( 102 | "MMM Do, hh:mm:ss" 103 | )} 104 |

    105 |

    ${message.text}

    106 |
    107 |
    ` 108 | ); 109 | 110 | chat.scrollTop = chat.scrollHeight - chat.clientHeight; 111 | } 112 | 113 | // Show the login page 114 | function showLogin(error = {}) { 115 | if (document.querySelectorAll(".login").length) { 116 | document 117 | .querySelector(".heading") 118 | .insertAdjacentHTML( 119 | "beforeend", 120 | `

    There was an error: ${error.message}

    ` 121 | ); 122 | } else { 123 | document.getElementById("app").innerHTML = loginHTML; 124 | } 125 | } 126 | 127 | // Shows the chat page 128 | function showChat() { 129 | document.getElementById("app").innerHTML = chatHTML; 130 | 131 | // Find the latest 10 messages. They will come with the newest first 132 | // which is why we have to reverse before adding them 133 | client 134 | .service("messages") 135 | .find({ 136 | query: { 137 | $params: { 138 | $sort: { createdAt: -1 }, 139 | $limit: 25 140 | }, 141 | total: 1, 142 | limit: 1, 143 | skip: 1, 144 | data: { 145 | text: 1, 146 | owner: { 147 | name: 1 148 | } 149 | } 150 | } 151 | }) 152 | .then(page => { 153 | page.data.reverse().forEach(addMessage); 154 | }); 155 | } 156 | 157 | // Retrieve email/password object from the login/signup page 158 | function getCredentials() { 159 | const user = { 160 | email: document.querySelector('[name="email"]').value, 161 | password: document.querySelector('[name="password"]').value 162 | }; 163 | 164 | return user; 165 | } 166 | 167 | // Log in either using the given email/password or the token from storage 168 | function login(credentials) { 169 | showChat(); 170 | } 171 | 172 | document.addEventListener("click", function(ev) { 173 | switch (ev.target.id) { 174 | case "signup": { 175 | const user = getCredentials(); 176 | 177 | // For signup, create a new user and then log them in 178 | client.service("users").create(user).then(() => login(user)); 179 | 180 | break; 181 | } 182 | case "login": { 183 | const user = getCredentials(); 184 | 185 | login(user); 186 | 187 | break; 188 | } 189 | case "logout": { 190 | client.logout().then(() => { 191 | document.getElementById("app").innerHTML = loginHTML; 192 | }); 193 | 194 | break; 195 | } 196 | } 197 | }); 198 | 199 | document.addEventListener("submit", function(ev) { 200 | if (ev.target.id === "send-message") { 201 | // This is the message text input field 202 | const input = document.querySelector('[name="text"]'); 203 | 204 | // Create a new message and then clear the input field 205 | client 206 | .service("messages") 207 | .create( 208 | { 209 | text: input.value 210 | }, 211 | { 212 | query: { 213 | text: 1, 214 | owner: { 215 | name: 1 216 | } 217 | } 218 | } 219 | ) 220 | .then(() => { 221 | input.value = ""; 222 | }); 223 | ev.preventDefault(); 224 | } 225 | }); 226 | 227 | // Listen to created events and add the new message in real-time 228 | client.service("messages").on("created", addMessage); 229 | 230 | // We will also see when new users get created in real-time 231 | client.service("users").on("created", addUser); 232 | 233 | login(); 234 | -------------------------------------------------------------------------------- /samples/chat/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vanilla JavaScript Feathers Chat 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 | 15 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /samples/posts/README.md: -------------------------------------------------------------------------------- 1 | # Nested services sample -------------------------------------------------------------------------------- /samples/posts/index.js: -------------------------------------------------------------------------------- 1 | const feathers = require("feathers"); 2 | const app = feathers(); 3 | const NextQL = require("../../../nextql/src"); 4 | const nextql = new NextQL(); 5 | const models = require("./models"); 6 | nextql.use(require("../../src"), { app: app }); 7 | Object.keys(models).forEach(k => nextql.model(k, models[k])); 8 | 9 | async function test(){ 10 | await app.service('users').create([ 11 | { _id: '2000', name: 'John' }, 12 | { _id: '2001', name: 'Marshall' }, 13 | { _id: '2002', name: 'David' }, 14 | ]).catch(() => true); //bypass if already created 15 | 16 | await app.service('posts').create([ 17 | { _id: '100', message: 'Hello', posterId: '2000', readerIds: ['2001', '2002'] } 18 | ]).catch(() => true); //bypass if already created 19 | 20 | const post = await app.service('posts').get('100',{ 21 | query: { 22 | _id: 1, 23 | message: 1, 24 | poster: { 25 | name: 1, 26 | posts: { 27 | message: 1 28 | } 29 | }, 30 | readers: { 31 | name: 1 32 | } 33 | } 34 | }); 35 | console.log(post); 36 | } 37 | 38 | test().then(() => true, console.log); 39 | 40 | -------------------------------------------------------------------------------- /samples/posts/models.js: -------------------------------------------------------------------------------- 1 | const NeDB = require("nedb"); 2 | const service = require("feathers-nedb"); 3 | 4 | // Create a NeDB instance 5 | const Posts = service({ 6 | Model: new NeDB({ 7 | filename: "./data/posts.db", 8 | autoload: true 9 | }) 10 | }); 11 | 12 | // Create a NeDB instance 13 | const Users = service({ 14 | Model: new NeDB({ 15 | filename: "./data/users.db", 16 | autoload: true 17 | }) 18 | }); 19 | 20 | module.exports = { 21 | users: { 22 | feathers: { 23 | path: "/users", 24 | service: Users 25 | }, 26 | fields: { 27 | _id: 1, 28 | name: 1, 29 | posts: "posts" 30 | }, 31 | computed: { 32 | posts(user){ 33 | return Posts.find({ 34 | query: { posterId: user._id } 35 | }); 36 | } 37 | } 38 | }, 39 | posts: { 40 | feathers: { 41 | path: "/posts", 42 | service: Posts 43 | }, 44 | fields: { 45 | _id: 1, 46 | message: 1, 47 | poster: "users", 48 | readers: "users" 49 | }, 50 | computed: { 51 | poster(post) { 52 | return Users.get(post.posterId); 53 | }, 54 | readers(post){ 55 | return Users.find({ 56 | query: { 57 | _id: {$in: post.readerIds} 58 | } 59 | }) 60 | } 61 | } 62 | } 63 | }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @example Extend nextql model with featherjs option 3 | * // { 4 | * // feathers: 'path' 5 | * // } 6 | */ 7 | 8 | class Service { 9 | constructor(options) { 10 | const { name, nextql } = options; 11 | 12 | this.name = name; 13 | this.nextql = nextql; 14 | } 15 | 16 | _execute(method, query, params) { 17 | const q = {}; 18 | q[this.name] = {}; 19 | q[this.name][method] = query; 20 | return this.nextql 21 | .execute(q, params) //featherjs params as context 22 | .then( 23 | result => 24 | (result && 25 | result[this.name] && 26 | result[this.name][method]) || 27 | null 28 | ); 29 | } 30 | 31 | find(params) { 32 | const query = Object.assign({}, params && params.query); 33 | return this._execute("find", query, params); 34 | } 35 | 36 | get(id, params) { 37 | const query = Object.assign({}, params && params.query); 38 | query.$params = { id }; 39 | return this._execute("get", query, params); 40 | } 41 | 42 | create(data, params) { 43 | const query = Object.assign({}, params && params.query); 44 | query.$params = { data }; 45 | return this._execute("create", query, params); 46 | } 47 | 48 | update(id, data, params) { 49 | const query = Object.assign({}, params && params.query); 50 | query.$params = { id, data }; 51 | return this._execute("update", query, params); 52 | } 53 | 54 | patch(id, data, params) { 55 | const query = Object.assign({}, params && params.query); 56 | query.$params = { id, data }; 57 | return this._execute("patch", query, params); 58 | } 59 | 60 | remove(id, params) { 61 | const query = Object.assign({}, params && params.query); 62 | query.$params = { id }; 63 | return this._execute("remove", query, params); 64 | } 65 | } 66 | 67 | function inject_feather_methods(name, options, service) { 68 | options.returns = Object.assign( 69 | { 70 | get: name, 71 | create: name, 72 | update: name, 73 | patch: name, 74 | remove: name, 75 | find() { 76 | if (service.paginate && service.paginate.default) { 77 | return { 78 | total: 1, 79 | limit: 1, 80 | skip: 1, 81 | data: name 82 | }; 83 | } else { 84 | return name; 85 | } 86 | } 87 | }, 88 | options.returns 89 | ); 90 | options.methods = Object.assign( 91 | { 92 | find(params, ctx) { 93 | return service.find({ 94 | query: ctx.query.$params 95 | }); 96 | }, 97 | 98 | get(params, ctx) { 99 | return service.get(params.id, ctx); 100 | }, 101 | 102 | create(params, ctx) { 103 | return service.create(params.data, ctx); 104 | }, 105 | 106 | update(params, ctx) { 107 | return service.update(params.id, params.data, ctx); 108 | }, 109 | 110 | patch(params, ctx) { 111 | return service.patch(params.id, params.data, ctx); 112 | }, 113 | 114 | remove(params, ctx) { 115 | return service.remove(params.id, ctx); 116 | } 117 | }, 118 | options.methods 119 | ); 120 | } 121 | 122 | module.exports = { 123 | install(nextql, options) { 124 | const app = options && options.app; 125 | if (!app) { 126 | throw new Error("Missing feathers app object"); 127 | } 128 | 129 | nextql.beforeCreate(options => { 130 | const name = options.name; 131 | if (options.feathers) { 132 | const { path, service } = options.feathers; 133 | app.use( 134 | path, 135 | new Service({ 136 | name: options.name, 137 | nextql 138 | }) 139 | ); 140 | 141 | if (service) { 142 | inject_feather_methods(name, options, service); 143 | } 144 | } 145 | }); 146 | } 147 | }; 148 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const NextQL = require("../../nextql"); 2 | const nextql = new NextQL(); 3 | const feathers = require("feathers"); 4 | const app = feathers(); 5 | const NeDB = require("nedb"); 6 | const service = require("feathers-nedb"); 7 | 8 | // Create a NeDB instance 9 | const Model = new NeDB({ 10 | filename: "./data/messages.db", 11 | autoload: true 12 | }); 13 | 14 | nextql.use(require("../src"), { 15 | app 16 | }); 17 | 18 | nextql.model("messages", { 19 | feathers: { 20 | path: "/messages", 21 | service: service({ Model }) 22 | }, 23 | fields: { 24 | _id: 1, 25 | text: 1, 26 | newText: 1, 27 | owner: { 28 | name: 1 29 | } 30 | }, 31 | computed: { 32 | owner() { 33 | return { 34 | name: "Giap Nguyen Huu" 35 | }; 36 | } 37 | } 38 | }); 39 | 40 | nextql.model("pmessages", { 41 | feathers: { 42 | path: "/pmessages", 43 | service: service({ 44 | Model, 45 | paginate: { 46 | default: 5, 47 | max: 25 48 | } 49 | }) 50 | }, 51 | fields: { 52 | _id: 1, 53 | text: 1, 54 | newText: 1, 55 | owner: { 56 | name: 1 57 | } 58 | }, 59 | computed: { 60 | owner() { 61 | return { 62 | name: "Giap Nguyen Huu" 63 | }; 64 | } 65 | } 66 | }); 67 | 68 | beforeAll(() => { 69 | return new Promise(function(ok) { 70 | Model.insert( 71 | [ 72 | { 73 | _id: 1, 74 | text: "Text 1" 75 | }, 76 | { _id: 2, text: "Text 2" } 77 | ], 78 | () => ok() 79 | ); 80 | }); 81 | }); 82 | 83 | afterAll(() => { 84 | return new Promise(function(ok) { 85 | Model.remove({}, { multi: true }, () => ok()); 86 | }); 87 | }); 88 | 89 | test("find messages", async () => { 90 | const messages = await app.service("messages").find({ 91 | query: { 92 | $params: { $limit: 2 }, 93 | _id: 1, 94 | text: 1, 95 | owner: { 96 | name: 1 97 | } 98 | } 99 | }); 100 | expect(messages.length).toBe(2); 101 | expect(messages[0].owner.name).toBe("Giap Nguyen Huu"); 102 | }); 103 | 104 | test("get message", async () => { 105 | const message = await app.service("messages").get(1, { 106 | query: { 107 | _id: 1, 108 | text: 1, 109 | owner: { 110 | name: 1 111 | } 112 | } 113 | }); 114 | 115 | expect(message).toMatchObject({ 116 | _id: 1, 117 | text: "Text 1", 118 | owner: { 119 | name: "Giap Nguyen Huu" 120 | } 121 | }); 122 | }); 123 | 124 | test("create message", async () => { 125 | const message = await app.service("messages").create( 126 | { 127 | _id: 3, 128 | text: "Text 3" 129 | }, 130 | { 131 | query: { 132 | _id: 1, 133 | text: 1, 134 | owner: { 135 | name: 1 136 | } 137 | } 138 | } 139 | ); 140 | 141 | expect(message).toMatchObject({ 142 | _id: 3, 143 | text: "Text 3", 144 | owner: { 145 | name: "Giap Nguyen Huu" 146 | } 147 | }); 148 | }); 149 | 150 | test("update message", async () => { 151 | await app.service("messages").update( 152 | 1, 153 | { 154 | text: "Update text 1" 155 | }, 156 | { 157 | query: {} 158 | } 159 | ); 160 | 161 | const message = await app.service("messages").get(1, { 162 | query: { 163 | _id: 1, 164 | text: 1, 165 | owner: { 166 | name: 1 167 | } 168 | } 169 | }); 170 | 171 | expect(message).toMatchObject({ 172 | _id: 1, 173 | text: "Update text 1", 174 | owner: { 175 | name: "Giap Nguyen Huu" 176 | } 177 | }); 178 | }); 179 | 180 | test("patch message", async () => { 181 | await app.service("messages").patch( 182 | 2, 183 | { 184 | newText: "Text 2" 185 | }, 186 | { 187 | query: {} 188 | } 189 | ); 190 | 191 | const message = await app.service("messages").get(2, { 192 | query: { 193 | _id: 1, 194 | text: 1, 195 | newText: 1, 196 | owner: { 197 | name: 1 198 | } 199 | } 200 | }); 201 | 202 | expect(message).toMatchObject({ 203 | _id: 2, 204 | text: "Text 2", 205 | newText: "Text 2", 206 | owner: { 207 | name: "Giap Nguyen Huu" 208 | } 209 | }); 210 | }); 211 | 212 | test("remove message", async () => { 213 | await app.service("messages").remove(3, { 214 | query: {} 215 | }); 216 | 217 | await app 218 | .service("messages") 219 | .get(3, { 220 | query: { 221 | _id: 1, 222 | text: 1, 223 | newText: 1, 224 | owner: { 225 | name: 1 226 | } 227 | } 228 | }) 229 | .catch(err => expect(err.message).toBe("No record found for id '3'")); 230 | }); 231 | 232 | test("support paginate", async function() { 233 | const messages = await app.service("pmessages").find({ 234 | query: { 235 | $params: { $limit: 2 }, 236 | total: 1, 237 | limit: 1, 238 | skip: 1, 239 | data: { 240 | _id: 1, 241 | text: 1, 242 | owner: { 243 | name: 1 244 | } 245 | } 246 | } 247 | }); 248 | 249 | expect(messages).toMatchObject({ 250 | limit: 2, 251 | skip: 0, 252 | data: [ 253 | { 254 | _id: 1, 255 | text: "Update text 1", 256 | owner: { 257 | name: "Giap Nguyen Huu" 258 | } 259 | }, 260 | { 261 | _id: 2, 262 | text: "Text 2", 263 | owner: { 264 | name: "Giap Nguyen Huu" 265 | } 266 | } 267 | ] 268 | }); 269 | }); 270 | --------------------------------------------------------------------------------