├── .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 |
2 |
3 |
About page
4 | Home
5 |
6 |
7 |
8 |
13 |
14 |
17 |
--------------------------------------------------------------------------------
/pages/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ user }}
5 |
See user
6 |
7 |
8 |
9 |
10 |
11 |
12 |
34 |
--------------------------------------------------------------------------------
/pages/users/_id.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ user }}
4 |
Home
5 |
6 |
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 |
--------------------------------------------------------------------------------