├── .gitignore ├── README.md ├── index.html ├── modules ├── nuxt-mongodb │ └── index.js └── nuxt-services │ ├── index.js │ ├── plugins │ ├── services.client.js │ └── services.server.js │ └── src │ └── utils.js ├── nuxt.config.js ├── package.json ├── pages ├── about.vue ├── index.vue └── users │ └── _id.vue ├── server.js ├── services ├── auth.js └── users.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node template 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # parcel-bundler cache (https://parceljs.org/) 62 | .cache 63 | 64 | # next.js build output 65 | .next 66 | 67 | # nuxt.js build output 68 | .nuxt 69 | 70 | # Nuxt generate 71 | dist 72 | 73 | # vuepress build output 74 | .vuepress/dist 75 | 76 | # Serverless directories 77 | .serverless 78 | 79 | # IDE 80 | .idea 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nuxt-services 2 | 3 | > Example of services features for Nuxt 3. 4 | 5 | ## Setup 6 | 7 | ``` 8 | yarn install 9 | ``` 10 | 11 | Make sure to have [MongoDB](https://www.mongodb.com) installed and running on your machine. 12 | 13 | ## Development 14 | 15 | ``` 16 | yarn dev 17 | ``` 18 | 19 | ## Production 20 | 21 | ``` 22 | yarn build 23 | yarn start 24 | ``` 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /modules/nuxt-mongodb/index.js: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | import { MongoClient, ObjectID } from 'mongodb' 3 | 4 | export default async function (options) { 5 | const mongodb = Object.assign({}, options, this.options.mongodb) 6 | 7 | if (!mongodb) throw new Error('No `mongodb` configuration found') 8 | if (!mongodb.url) throw new Error('No `mongodb.url` configuration found') 9 | if (!mongodb.dbName) throw new Error('No `mongodb.dbName` configuration found') 10 | 11 | // Defaults 12 | mongodb.findLimitDefault = mongodb.findLimitDefault || 20 13 | mongodb.findLimitMax = mongodb.findLimitMax || 100 14 | 15 | consola.info(`Connecting to ${mongodb.url}...`) 16 | const client = await MongoClient.connect(mongodb.url, { useNewUrlParser: true, ...mongodb.options }) 17 | const db = client.db(mongodb.dbName) 18 | consola.info(`Connected to ${mongodb.dbName} database`) 19 | 20 | this.nuxt.hook('services:context', (context) => { 21 | context.$mongodb = db 22 | }) 23 | this.nuxt.hook('build:done', () => { 24 | consola.info(`Closing ${mongodb.dbName} database`) 25 | client.close() 26 | }) 27 | } 28 | 29 | -------------------------------------------------------------------------------- /modules/nuxt-services/index.js: -------------------------------------------------------------------------------- 1 | import { resolve, join } from 'path' 2 | import { promisify } from 'util' 3 | import devalue from '@nuxtjs/devalue' 4 | import WebSocket from 'ws' 5 | import { getServiceMethods, loadServices } from './src/utils' 6 | 7 | const glob = promisify(require('glob')) 8 | 9 | export default async function() { 10 | /* 11 | * Fetch services 12 | */ 13 | const servicesPath = resolve(this.options.srcDir, 'services') 14 | const files = await glob(join(servicesPath, '/**/*.js')) 15 | this.options.watch = this.options.watch.concat(files) 16 | const Services = {} 17 | const ServicesMethods = {} 18 | 19 | files.map(path => { 20 | const serviceKey = path 21 | .replace(servicesPath, '') 22 | .replace(/^\//, '') 23 | .replace(/\.js$/, '') 24 | const Service = this.nuxt.resolver.requireModule(path) || {} 25 | 26 | Services[serviceKey] = Service 27 | ServicesMethods[serviceKey] = getServiceMethods(Service) 28 | }) 29 | /* 30 | ** Add plugin 31 | */ 32 | const url = `ws://${this.options.server.host}:${this.options.server.port}` 33 | this.addPlugin({ 34 | filename: 'services.ws.client.js', 35 | src: join(__dirname, 'plugins/services.client.js'), 36 | ssr: false, 37 | options: { 38 | url, 39 | ServicesMethods 40 | } 41 | }) 42 | this.addPlugin({ 43 | filename: 'services.ws.server.js', 44 | src: join(__dirname, 'plugins/services.server.js') 45 | }) 46 | // TODO: get the context from `server middleware` 47 | // where @nuxtjs/axios could also instanciate himself 48 | this.nuxt.hook('vue-renderer:ssr:context', async (ssrContext) => { 49 | const context = ssrContext 50 | 51 | await this.nuxt.callHook('services:context', context) 52 | ssrContext.services = loadServices(Services, context) 53 | }) 54 | /* 55 | ** Create WS server 56 | */ 57 | this.nuxt.hook('listen', server => { 58 | const wss = new WebSocket.Server({ server }) 59 | 60 | wss.on('connection', async (ws, req) => { 61 | const context = { req, ws } 62 | console.log('new connection') 63 | await this.nuxt.callHook('services:context', context) 64 | const services = loadServices(Services, context) 65 | 66 | ws.on('error', err => Consola.error(err)) 67 | 68 | ws.on('message', async msg => { 69 | let obj 70 | try { 71 | obj = (0,eval)(`(${msg})`) 72 | } catch (e) { 73 | return // Ignore it 74 | } 75 | if (typeof obj.challenge === 'undefined') 76 | return consola.error('No challenge given to', obj) 77 | 78 | let data = null 79 | let error = null 80 | 81 | switch (obj.action) { 82 | case 'call': 83 | try { 84 | let serviceModule = services 85 | obj.module.split('/').forEach((m) => { 86 | serviceModule = serviceModule[m] 87 | }) 88 | data = await serviceModule[obj.method](...obj.args) 89 | } catch (e) { 90 | if (this.options.dev) consola.error(e) 91 | error = JSON.parse(JSON.stringify(e, Object.getOwnPropertyNames(e))) 92 | if (!this.options.dev) delete error.stack 93 | } 94 | break 95 | default: 96 | } 97 | 98 | const payload = { 99 | action: 'return', 100 | challenge: obj.challenge || 0, 101 | data, 102 | error 103 | } 104 | 105 | ws.send(devalue(payload)) 106 | }) 107 | }) 108 | consola.info('Websockets server ready for services') 109 | }) 110 | } 111 | 112 | export class NuxtService { 113 | constructor ({ services }) { 114 | this.$services = services 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /modules/nuxt-services/plugins/services.client.js: -------------------------------------------------------------------------------- 1 | import devalue from '@nuxtjs/devalue' 2 | 3 | class ClientRPC { 4 | constructor() { 5 | this.ws = null 6 | this.challenge = 0 7 | this.autoReconnectInterval = 2000 // 1 second 8 | this.returns = new Map() 9 | this.connected = false 10 | this.connect() 11 | } 12 | 13 | connect() { 14 | this.ws = new WebSocket('<%= options.url %>') 15 | this.ws.onopen = () => { 16 | this.connected = true 17 | } 18 | this.ws.onclose = (e) => { 19 | this.connected = false 20 | switch (e.code){ 21 | case 1000: // CLOSE_NORMAL 22 | // console.log('WebSocket: closed') 23 | break; 24 | default: // Abnormal closure 25 | this.reconnect(e) 26 | break; 27 | } 28 | } 29 | this.ws.onerror = (e) => { 30 | switch (e.code){ 31 | case 'ECONNREFUSED': 32 | this.reconnect(e) 33 | break; 34 | default: 35 | // TODO: call service error handler ($services.on('error', ...)) 36 | break; 37 | } 38 | } 39 | this.ws.onmessage = this.onMessage.bind(this) 40 | } 41 | 42 | reconnect(e) { 43 | // console.log(`WebSocketClient: retry in ${this.autoReconnectInterval}ms`, e) 44 | setTimeout(() => { 45 | // console.log('WebSocketClient: reconnecting...') 46 | this.connect() 47 | }, this.autoReconnectInterval) 48 | } 49 | 50 | onMessage(msg) { 51 | let obj 52 | 53 | try { 54 | obj = (0, eval)(`(${msg.data})`) 55 | } catch(e) { 56 | console.error('Error', e, msg.data) 57 | return 58 | } 59 | switch (obj.action) { 60 | case 'return': 61 | if (obj.challenge) { 62 | const [resolve, reject] = this.returns.get(obj.challenge) 63 | if (obj.error && reject) { 64 | reject(obj.error) 65 | } 66 | if (resolve) { 67 | resolve(obj.data) 68 | } 69 | this.returns.delete(obj.challenge) 70 | } 71 | break; 72 | default: 73 | } 74 | } 75 | async callMethod(module, name, ...args) { 76 | if (!this.connected) { 77 | // console.log('WS not connected, retrying in a sec...') 78 | await new Promise((resolve) => setTimeout(resolve, 1000)) 79 | return this.callMethod(module, name, ...args) 80 | } 81 | 82 | const payload = { 83 | action: 'call', 84 | module, 85 | method: name, 86 | args: args, 87 | challenge: ++this.challenge 88 | } 89 | 90 | const data = new Promise((resolve, reject) => this.returns.set(this.challenge, [resolve, reject])) 91 | 92 | this.ws.send(devalue(payload)) 93 | 94 | return data 95 | } 96 | } 97 | 98 | export default async (ctx, inject) => { 99 | const services = new ClientRPC() 100 | 101 | <% Object.keys(options.ServicesMethods).forEach((moduleNamespace) => { 102 | const methods = options.ServicesMethods[moduleNamespace] 103 | 104 | const modules = [] 105 | moduleNamespace.split('/').forEach((m) => { 106 | modules.push(m) 107 | const module = modules.map((m) => `['${m}']`).join('') %> 108 | services<%= module %> = services<%= module %> || {}<% 109 | }) 110 | methods.forEach((method) => { 111 | const module = modules.concat(method).map((m) => `['${m}']`).join('') %> 112 | services<%= module %> = services.callMethod.bind(services, '<%= moduleNamespace %>', '<%= method %>')<% 113 | }) 114 | }) %> 115 | inject('services', services) 116 | ctx.$services = services 117 | } 118 | -------------------------------------------------------------------------------- /modules/nuxt-services/plugins/services.server.js: -------------------------------------------------------------------------------- 1 | export default async (ctx, inject) => { 2 | if (process.server) { 3 | inject('services', ctx.ssrContext.services) 4 | ctx.$services = ctx.ssrContext.services 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /modules/nuxt-services/src/utils.js: -------------------------------------------------------------------------------- 1 | const filterPublicMethods = (Service) => (m) => { 2 | if (m[0] === '_' || m === 'constructor') return false 3 | if (typeof Service[m] !== 'function') return false 4 | 5 | return true 6 | } 7 | 8 | export function getServiceMethods(Service) { 9 | const methods = Object.getOwnPropertyNames(Service.prototype).filter(filterPublicMethods(Service.prototype)) 10 | 11 | // Later on if extends NuxtService 12 | // const ParentService = Object.getPrototypeOf(Service.prototype) 13 | // const parentMethods = Object.getOwnPropertyNames(ParentService).filter(filterPublicMethods(ParentService)) 14 | // methods = methods.concat(parentMethods).unique()? 15 | 16 | return methods 17 | } 18 | 19 | export function loadServices(Services, ssrContext) { 20 | let services = {} 21 | 22 | // For each Service, instanciate it 23 | Object.keys(Services).forEach((serviceNamespace) => { 24 | let s = services 25 | const keys = serviceNamespace.split('/') 26 | 27 | keys.forEach((key, i) => { 28 | if (i + 1 < keys.length) { 29 | s[key] = s[key] || {} 30 | s = s[key] 31 | return 32 | } 33 | // Expose every public methods of the service instance definition 34 | const Service = Services[serviceNamespace] 35 | s[key] = new Service({ ...ssrContext, services }) 36 | }) 37 | }) 38 | 39 | return services 40 | } 41 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | modules: [ 3 | '~/modules/nuxt-mongodb', 4 | '~/modules/nuxt-services' 5 | ], 6 | mongodb: { 7 | // url is required 8 | url: 'mongodb://localhost:27017', 9 | dbName: 'my-db' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-services", 3 | "description": "", 4 | "version": "0.1.0", 5 | "scripts": { 6 | "dev": "nuxt", 7 | "build": "nuxt build", 8 | "start": "nuxt start", 9 | "generate": "nuxt generate" 10 | }, 11 | "dependencies": { 12 | "@nuxtjs/devalue": "^1.2.0", 13 | "mongodb": "^3.1.10", 14 | "nuxt-start-edge": "^2.4.0-25753460.5b58272d", 15 | "ws": "^6.1.2" 16 | }, 17 | "devDependencies": { 18 | "nuxt-edge": "^2.4.0-25753460.5b58272d" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pages/about.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 34 | -------------------------------------------------------------------------------- /pages/users/_id.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws'); 2 | 3 | 4 | class RPC { 5 | constructor() { 6 | 7 | } 8 | 9 | testCall(x, y) { 10 | console.log("Test Call called", x, y); 11 | 12 | return x + y; 13 | } 14 | } 15 | 16 | const wss = new WebSocket.Server({ port: 8081 }); 17 | 18 | wss.on('connection', function connection(ws) { 19 | 20 | ws.state = { 21 | rpc: new RPC() 22 | } 23 | 24 | console.log("New client"); 25 | 26 | ws.on('error', function() { 27 | console.log("Got error"); 28 | }) 29 | 30 | ws.on('message', function incoming(message) { 31 | 32 | try { 33 | var obj = JSON.parse(message) 34 | } catch(e) { 35 | return; 36 | } 37 | 38 | let ret = null; 39 | 40 | switch (obj.action) { 41 | case "call": 42 | try { 43 | ret = ws.state.rpc[obj.method](...obj.args); 44 | } catch(e) { 45 | ret = e; 46 | } 47 | 48 | break; 49 | default: 50 | } 51 | 52 | const payload = { 53 | action: "return", 54 | challenge: obj.challenge || 0, 55 | data: ret 56 | } 57 | 58 | ws.send(JSON.stringify(payload)); 59 | }); 60 | }); 61 | 62 | -------------------------------------------------------------------------------- /services/auth.js: -------------------------------------------------------------------------------- 1 | import { NuxtService } from '../modules/nuxt-services' 2 | import Cookies from 'cookies' 3 | 4 | export default class AuthService extends NuxtService { 5 | // Constructor is called once on server side and kept alive with socket 6 | constructor(ctx) { 7 | super(ctx) 8 | const { ws, req } = ctx 9 | // this.$axios = $axios 10 | // this.$axios = $axios 11 | const cookies = Cookies(req) 12 | 13 | // Get jwt cookie 14 | const jwt = cookies.get('jwt') 15 | this.$axios.setToken(jwt, 'Bearer') 16 | } 17 | 18 | async getUser (req, res) { 19 | // Check if JWT is good 20 | try { 21 | // TODO 22 | const user = await this.$services.me.show() 23 | // Set expiration in 3 months 24 | const expires = new Date() 25 | expires.setMonth(expires.getMonth() + 3) 26 | // TODO: Find a way to set cookie from WS 27 | // Update jwt cookie 28 | cookies.set('jwt', user.token, { 29 | expires, 30 | httpOnly: true 31 | }) 32 | delete user.token 33 | 34 | return user 35 | } catch (err) { 36 | if (err.response && err.response.status === 401) { 37 | // cookies.set('jwt') // Remove jwt cookie 38 | // Find a way to remove cookie from WS? 39 | this.$axios.setToken(false) 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /services/users.js: -------------------------------------------------------------------------------- 1 | import { ObjectId } from 'mongodb' 2 | import { NuxtService } from '../modules/nuxt-services' 3 | 4 | export default class UsersService extends NuxtService { 5 | constructor({ nuxt }) { 6 | this.$db = nuxt.$db 7 | } 8 | 9 | async list() { 10 | const users = await this.$db.collection('users').find({}).toArray() 11 | 12 | users.forEach(user => user._id = String(user._id)) 13 | return users 14 | } 15 | 16 | async get(id) { 17 | const user = await this.$db.collection('users').findOne({ _id: ObjectId(id) }) 18 | 19 | user._id = String(user._id) 20 | return user 21 | } 22 | 23 | async create(user) { 24 | user.createdAt = new Date() 25 | user.updatedAt = new Date() 26 | 27 | await this.$db.collection('users').insertOne(user) 28 | 29 | user._id = String(user._id) 30 | return user 31 | } 32 | 33 | async remove(id) { 34 | const result = await this.$db.collection('users').deleteOne({ _id: ObjectId(id) }) 35 | 36 | return !!result.deletedCount 37 | } 38 | } 39 | --------------------------------------------------------------------------------