Initial commit.
This commit is contained in:
8
server/.gitignore
vendored
Normal file
8
server/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
.DS_Store
|
||||
node_modules/
|
||||
coverage/
|
||||
npm-debug.log
|
||||
stats.json
|
||||
yarn-error.log
|
||||
|
||||
data/
|
||||
21
server/LICENSE
Normal file
21
server/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Ice Services
|
||||
|
||||
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.
|
||||
BIN
server/logo.png
Normal file
BIN
server/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
31
server/mixins/db.mixin.js
Normal file
31
server/mixins/db.mixin.js
Normal file
@@ -0,0 +1,31 @@
|
||||
"use strict";
|
||||
|
||||
const path = require("path");
|
||||
const mkdir = require("mkdirp").sync;
|
||||
|
||||
const DbService = require("moleculer-db");
|
||||
|
||||
//process.env.MONGO_URI = "mongodb://localhost/conduit";
|
||||
|
||||
module.exports = function(collection) {
|
||||
if (process.env.MONGO_URI) {
|
||||
// Mongo adapter
|
||||
const MongoAdapter = require("moleculer-db-adapter-mongo");
|
||||
|
||||
return {
|
||||
mixins: [DbService],
|
||||
adapter: new MongoAdapter(process.env.MONGO_URI),
|
||||
collection
|
||||
};
|
||||
}
|
||||
|
||||
// --- NeDB fallback DB adapter
|
||||
|
||||
// Create data folder
|
||||
mkdir(path.resolve("./data"));
|
||||
|
||||
return {
|
||||
mixins: [DbService],
|
||||
adapter: new DbService.MemoryAdapter({ filename: `./data/${collection}.db` })
|
||||
};
|
||||
};
|
||||
17
server/moleculer.config.js
Normal file
17
server/moleculer.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
"use strict";
|
||||
|
||||
const os = require("os");
|
||||
|
||||
module.exports = {
|
||||
// It will be unique when scale up instances in Docker or on local computer
|
||||
nodeID: os.hostname().toLowerCase() + "-" + process.pid,
|
||||
|
||||
logger: true,
|
||||
logLevel: "info",
|
||||
|
||||
//transporter: "nats://localhost:4222",
|
||||
|
||||
cacher: "memory",
|
||||
|
||||
metrics: true
|
||||
};
|
||||
8612
server/package-lock.json
generated
Normal file
8612
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
server/package.json
Normal file
52
server/package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "moleculer-realworld-example-app",
|
||||
"version": "1.0.0",
|
||||
"description": "RealWorld example app with Moleculer microservices framework",
|
||||
"scripts": {
|
||||
"dev": "moleculer-runner --repl --hot services",
|
||||
"start": "moleculer-runner",
|
||||
"deps": "npm-check -u",
|
||||
"ci": "jest --watch",
|
||||
"test": "jest --coverage",
|
||||
"lint": "eslint services",
|
||||
"docker:build": "docker build -t conduit ."
|
||||
},
|
||||
"keywords": [
|
||||
"microservices",
|
||||
"moleculer",
|
||||
"realworld"
|
||||
],
|
||||
"author": "",
|
||||
"devDependencies": {
|
||||
"eslint": "4.11.0",
|
||||
"jest": "21.2.1",
|
||||
"jest-cli": "21.2.1",
|
||||
"moleculer-repl": "^0.3.0",
|
||||
"npm-check": "5.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "1.0.3",
|
||||
"jsonwebtoken": "8.1.0",
|
||||
"lodash": "4.17.4",
|
||||
"moleculer": "^0.11.0",
|
||||
"moleculer-db": "0.7.0",
|
||||
"moleculer-db-adapter-mongo": "0.1.6",
|
||||
"moleculer-web": "0.6.0-beta7",
|
||||
"nats": "0.7.24",
|
||||
"slug": "0.9.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ice-services/moleculer-realworld-example-app.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.x.x"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"rootDir": "./services",
|
||||
"roots": [
|
||||
"../test"
|
||||
]
|
||||
}
|
||||
}
|
||||
4
server/public/asset-manifest.json
Normal file
4
server/public/asset-manifest.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"main.js": "static/js/main.cd99036f.js",
|
||||
"main.js.map": "static/js/main.cd99036f.js.map"
|
||||
}
|
||||
BIN
server/public/favicon.ico
Normal file
BIN
server/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
1
server/public/index.html
Normal file
1
server/public/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="shortcut icon" href="/favicon.ico"><link rel="stylesheet" href="//demo.productionready.io/main.css"><link href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" rel="stylesheet"><link href="//fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic" rel="stylesheet"><title>Conduit</title></head><body><div id="root"></div><script type="text/javascript" src="/static/js/main.41b57957.js"></script></body></html>
|
||||
BIN
server/rw-logo.png
Normal file
BIN
server/rw-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
93
server/server_readme.md
Normal file
93
server/server_readme.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# 
|
||||
|
||||
> ### [Moleculer](http://moleculer.services/) codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld) spec and API.
|
||||
|
||||
This repo is functionality complete — PRs and issues welcome!
|
||||
|
||||
**Live demo on Glitch: https://realworld-moleculer.glitch.me**
|
||||
|
||||
Glitch project: https://glitch.com/edit/#!/realworld-moleculer
|
||||
|
||||
*[React + Redux](https://github.com/icebob/react-redux-realworld-example-app) front-end UI is included.*
|
||||
*For more information on how to this works with other frontends/backends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repo.*
|
||||
|
||||
## Getting started
|
||||
|
||||
### To get the Node server running locally:
|
||||
|
||||
- Clone this repo
|
||||
- `npm install` to install all required dependencies
|
||||
- `npm run dev` to start the local server
|
||||
- the API is available at http://localhost:3000/api
|
||||
|
||||
Alternately, to quickly try out this repo in the cloud, you can
|
||||
|
||||
[](https://glitch.com/edit/#!/remix/realworld-moleculer)
|
||||
|
||||
#### MongoDB persistent store
|
||||
Basically the services stores data in an NeDB persistent file storage in the `./data` folder. If you have to use MongoDB, set the `MONGO_URI` environment variable.
|
||||
```
|
||||
MONGO_URI=mongodb://localhost/conduit
|
||||
```
|
||||
|
||||
#### Multiple instances
|
||||
You can run multiple instances of services. In this case you need to use a transporter i.e.: [NATS](https://nats.io). NATS is a lightweight & fast message broker. Download it and start with `gnatsd` command. After it started, set the `TRANSPORTER` env variable and start services.
|
||||
```
|
||||
TRANSPORTER=nats://localhost:4222
|
||||
```
|
||||
|
||||
### To get the Node server running locally with Docker
|
||||
|
||||
1. Checkout the repo `git clone https://github.com/ice-services/moleculer-realworld-example-app.git`
|
||||
2. `cd moleculer-realworld-example-app`
|
||||
3. Start with docker-compose: `docker-compose up -d`
|
||||
|
||||
It starts all services in separated containers, a NATS server for communication, a MongoDB server for database and a [Traefik](https://traefik.io/) reverse proxy
|
||||
4. Open the http://docker-ip:3000
|
||||
5. Scale up services
|
||||
|
||||
`docker-compose scale api=3 articles=2 users=2 comments=2 follows=2 favorites=2`
|
||||
|
||||
## Code Overview
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [moleculer](https://github.com/ice-services/moleculer) - Microservices framework for NodeJS
|
||||
- [moleculer-web](https://github.com/ice-services/moleculer-web) - Official API Gateway service for Moleculer
|
||||
- [moleculer-db](https://github.com/ice-services/moleculer-db/tree/master/packages/moleculer-db#readme) - Database store service for Moleculer
|
||||
- [moleculer-db-adapter-mongo](https://github.com/ice-services/moleculer-db/tree/master/packages/moleculer-db-adapter-mongo#readme) - Database store service for MongoDB *(optional)*
|
||||
- [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) - For generating JWTs used by authentication
|
||||
- [bcrypt](https://github.com/kelektiv/node.bcrypt.js) - Hashing user password
|
||||
- [lodash](https://github.com/lodash/lodash) - Utility library
|
||||
- [slug](https://github.com/dodo/node-slug) - For encoding titles into a URL-friendly format
|
||||
- [nats](https://github.com/dodo/node-slug) - [NATS](https://nats.io) transport driver for Moleculer *(optional)*
|
||||
|
||||
### Application Structure
|
||||
|
||||
- `moleculer.config.js` - Moleculer ServiceBroker configuration file.
|
||||
- `services/` - This folder contains the services.
|
||||
- `public/` - This folder contains the front-end static files.
|
||||
- `data/` - This folder contains the NeDB database files.
|
||||
|
||||
## Test
|
||||
|
||||
**Tested with [realworld-server-tester](https://github.com/agrison/realworld-server-tester).**
|
||||
|
||||
*Local tests is missing currently.*
|
||||
```
|
||||
$ npm test
|
||||
```
|
||||
|
||||
In development with watching
|
||||
|
||||
```
|
||||
$ npm run ci
|
||||
```
|
||||
|
||||
## License
|
||||
This project is available under the [MIT license](https://tldrlegal.com/license/mit-license).
|
||||
|
||||
## Contact
|
||||
Copyright (c) 2016-2017 Ice-Services
|
||||
|
||||
[](https://github.com/ice-services) [](https://twitter.com/MoleculerJS)
|
||||
171
server/services/api.service.js
Normal file
171
server/services/api.service.js
Normal file
@@ -0,0 +1,171 @@
|
||||
"use strict";
|
||||
|
||||
const _ = require("lodash");
|
||||
const ApiGateway = require("moleculer-web");
|
||||
const { UnAuthorizedError } = ApiGateway.Errors;
|
||||
|
||||
module.exports = {
|
||||
name: "api",
|
||||
mixins: [ApiGateway],
|
||||
|
||||
settings: {
|
||||
port: process.env.PORT || 3000,
|
||||
|
||||
routes: [{
|
||||
path: "/api",
|
||||
|
||||
authorization: true,
|
||||
|
||||
aliases: {
|
||||
// Login
|
||||
"POST /users/login": "users.login",
|
||||
|
||||
// Users
|
||||
"REST /users": "users",
|
||||
|
||||
// Current user
|
||||
"GET /user": "users.me",
|
||||
"PUT /user": "users.updateMyself",
|
||||
|
||||
// Articles
|
||||
"GET /articles/feed": "articles.feed",
|
||||
"REST /articles": "articles",
|
||||
"GET /tags": "articles.tags",
|
||||
|
||||
// Comments
|
||||
"GET /articles/:slug/comments": "articles.comments",
|
||||
"POST /articles/:slug/comments": "articles.addComment",
|
||||
"PUT /articles/:slug/comments/:commentID": "articles.updateComment",
|
||||
"DELETE /articles/:slug/comments/:commentID": "articles.removeComment",
|
||||
|
||||
// Favorites
|
||||
"POST /articles/:slug/favorite": "articles.favorite",
|
||||
"DELETE /articles/:slug/favorite": "articles.unfavorite",
|
||||
|
||||
// Profile
|
||||
"GET /profiles/:username": "users.profile",
|
||||
"POST /profiles/:username/follow": "users.follow",
|
||||
"DELETE /profiles/:username/follow": "users.unfollow",
|
||||
},
|
||||
|
||||
// Disable to call not-mapped actions
|
||||
mappingPolicy: "restrict",
|
||||
|
||||
// Set CORS headers
|
||||
cors: true,
|
||||
|
||||
// Parse body content
|
||||
bodyParsers: {
|
||||
json: {
|
||||
strict: false
|
||||
},
|
||||
urlencoded: {
|
||||
extended: false
|
||||
}
|
||||
}
|
||||
}],
|
||||
|
||||
assets: {
|
||||
folder: "./public"
|
||||
},
|
||||
|
||||
// logRequestParams: "info",
|
||||
// logResponseData: "info",
|
||||
|
||||
onError(req, res, err) {
|
||||
// Return with the error as JSON object
|
||||
res.setHeader("Content-type", "application/json; charset=utf-8");
|
||||
res.writeHead(err.code || 500);
|
||||
|
||||
if (err.code == 422) {
|
||||
let o = {};
|
||||
err.data.forEach(e => {
|
||||
let field = e.field.split(".").pop();
|
||||
o[field] = e.message;
|
||||
});
|
||||
|
||||
res.end(JSON.stringify({ errors: o }, null, 2));
|
||||
} else {
|
||||
const errObj = _.pick(err, ["name", "message", "code", "type", "data"]);
|
||||
res.end(JSON.stringify(errObj, null, 2));
|
||||
}
|
||||
this.logResponse(req, res, err? err.ctx : null);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Authorize the request
|
||||
*
|
||||
* @param {Context} ctx
|
||||
* @param {Object} route
|
||||
* @param {IncomingRequest} req
|
||||
* @returns {Promise}
|
||||
*/
|
||||
authorize(ctx, route, req) {
|
||||
let token;
|
||||
if (req.headers.authorization) {
|
||||
let type = req.headers.authorization.split(" ")[0];
|
||||
if (type === "Token" || type === "Bearer")
|
||||
token = req.headers.authorization.split(" ")[1];
|
||||
}
|
||||
|
||||
return this.Promise.resolve(token)
|
||||
.then(token => {
|
||||
if (token) {
|
||||
// Verify JWT token
|
||||
return ctx.call("users.resolveToken", { token })
|
||||
.then(user => {
|
||||
if (user) {
|
||||
this.logger.info("Authenticated via JWT: ", user.username);
|
||||
// Reduce user fields (it will be transferred to other nodes)
|
||||
ctx.meta.user = _.pick(user, ["_id", "username", "email", "image"]);
|
||||
ctx.meta.token = token;
|
||||
}
|
||||
return user;
|
||||
})
|
||||
.catch(err => {
|
||||
// Ignored because we continue processing if user is not exist
|
||||
return null;
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(user => {
|
||||
if (req.$endpoint.action.auth == "required" && !user)
|
||||
return this.Promise.reject(new UnAuthorizedError());
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert ValidationError to RealWorld.io result
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @param {*} err
|
||||
*/
|
||||
/*sendError(req, res, err) {
|
||||
if (err.code == 422) {
|
||||
res.setHeader("Content-type", "application/json; charset=utf-8");
|
||||
res.writeHead(422);
|
||||
let o = {};
|
||||
err.data.forEach(e => {
|
||||
let field = e.field.split(".").pop();
|
||||
o[field] = e.message;
|
||||
});
|
||||
return res.end(JSON.stringify({
|
||||
errors: o
|
||||
}, null, 2));
|
||||
|
||||
}
|
||||
|
||||
return this._sendError(req, res, err);
|
||||
}*/
|
||||
},
|
||||
|
||||
created() {
|
||||
// Pointer to the original function
|
||||
//this._sendError = ApiGateway.methods.sendError.bind(this);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
614
server/services/articles.service.js
Normal file
614
server/services/articles.service.js
Normal file
@@ -0,0 +1,614 @@
|
||||
"use strict";
|
||||
|
||||
const { MoleculerClientError } = require("moleculer").Errors;
|
||||
const { ForbiddenError } = require("moleculer-web").Errors;
|
||||
|
||||
const _ = require("lodash");
|
||||
const slug = require("slug");
|
||||
const DbService = require("../mixins/db.mixin");
|
||||
|
||||
module.exports = {
|
||||
name: "articles",
|
||||
mixins: [DbService("articles")],
|
||||
|
||||
/**
|
||||
* Default settings
|
||||
*/
|
||||
settings: {
|
||||
fields: ["_id", "title", "slug", "description", "body", "tagList", "createdAt", "updatedAt", "favorited", "favoritesCount", "author", "comments"],
|
||||
|
||||
// Populates
|
||||
populates: {
|
||||
author: {
|
||||
action: "users.get",
|
||||
params: {
|
||||
fields: ["username", "bio", "image"]
|
||||
}
|
||||
},
|
||||
comments: {
|
||||
action: "comments.get",
|
||||
params: {
|
||||
fields: ["_id", "body", "author"],
|
||||
populates: ["author"]
|
||||
}
|
||||
},
|
||||
favorited(ids, articles, rule, ctx) {
|
||||
if (ctx.meta.user)
|
||||
return this.Promise.all(articles.map(article => ctx.call("favorites.has", { article: article._id.toString(), user: ctx.meta.user._id.toString() }).then(res => article.favorited = res)));
|
||||
else {
|
||||
articles.forEach(article => article.favorited = false);
|
||||
return this.Promise.resolve();
|
||||
}
|
||||
},
|
||||
favoritesCount(ids, articles, rule, ctx) {
|
||||
return this.Promise.all(articles.map(article => ctx.call("favorites.count", { article: article._id.toString() }).then(count => article.favoritesCount = count)));
|
||||
}
|
||||
},
|
||||
|
||||
// Validation schema for new entities
|
||||
entityValidator: {
|
||||
title: { type: "string", min: 1 },
|
||||
description: { type: "string", min: 1 },
|
||||
body: { type: "string", min: 1 },
|
||||
tagList: { type: "array", items: "string", optional: true },
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
actions: {
|
||||
|
||||
/**
|
||||
* Create a new article.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {Object} article - Article entity
|
||||
*
|
||||
* @returns {Object} Created entity
|
||||
*/
|
||||
create: {
|
||||
auth: "required",
|
||||
params: {
|
||||
article: { type: "object" }
|
||||
},
|
||||
handler(ctx) {
|
||||
let entity = ctx.params.article;
|
||||
return this.validateEntity(entity)
|
||||
.then(() => {
|
||||
|
||||
entity.slug = slug(entity.title, { lower: true }) + "-" + (Math.random() * Math.pow(36, 6) | 0).toString(36);
|
||||
entity.author = ctx.meta.user._id.toString();
|
||||
entity.createdAt = new Date();
|
||||
entity.updatedAt = new Date();
|
||||
|
||||
return this.adapter.insert(entity)
|
||||
.then(doc => this.transformDocuments(ctx, { populate: ["author", "favorited", "favoritesCount"]}, doc))
|
||||
.then(entity => this.transformResult(ctx, entity, ctx.meta.user))
|
||||
.then(json => this.entityChanged("created", json, ctx).then(() => json));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an article.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} id - Article ID
|
||||
* @param {Object} article - Article modified fields
|
||||
*
|
||||
* @returns {Object} Updated entity
|
||||
*/
|
||||
update: {
|
||||
auth: "required",
|
||||
params: {
|
||||
id: { type: "string" },
|
||||
article: { type: "object", props: {
|
||||
title: { type: "string", min: 1, optional: true },
|
||||
description: { type: "string", min: 1, optional: true },
|
||||
body: { type: "string", min: 1, optional: true },
|
||||
tagList: { type: "array", items: "string", optional: true },
|
||||
} }
|
||||
},
|
||||
handler(ctx) {
|
||||
let newData = ctx.params.article;
|
||||
newData.updatedAt = new Date();
|
||||
// the 'id' is the slug
|
||||
return this.Promise.resolve(ctx.params.id)
|
||||
.then(slug => this.findBySlug(slug))
|
||||
.then(article => {
|
||||
if (!article)
|
||||
return this.Promise.reject(new MoleculerClientError("Article not found", 404));
|
||||
|
||||
if (article.author !== ctx.meta.user._id.toString())
|
||||
return this.Promise.reject(new ForbiddenError());
|
||||
|
||||
const update = {
|
||||
"$set": newData
|
||||
};
|
||||
|
||||
return this.adapter.updateById(article._id, update);
|
||||
})
|
||||
.then(doc => this.transformDocuments(ctx, { populate: ["author", "favorited", "favoritesCount"]}, doc))
|
||||
.then(entity => this.transformResult(ctx, entity, ctx.meta.user))
|
||||
.then(json => this.entityChanged("updated", json, ctx).then(() => json));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* List articles with pagination.
|
||||
*
|
||||
* @actions
|
||||
* @param {String} tag - Filter for 'tag'
|
||||
* @param {String} author - Filter for author ID
|
||||
* @param {String} favorited - Filter for favorited author
|
||||
* @param {Number} limit - Pagination limit
|
||||
* @param {Number} offset - Pagination offset
|
||||
*
|
||||
* @returns {Object} List of articles
|
||||
*/
|
||||
list: {
|
||||
cache: {
|
||||
keys: ["#token", "tag", "author", "favorited", "limit", "offset"]
|
||||
},
|
||||
params: {
|
||||
tag: { type: "string", optional: true },
|
||||
author: { type: "string", optional: true },
|
||||
favorited: { type: "string", optional: true },
|
||||
limit: { type: "number", optional: true, convert: true },
|
||||
offset: { type: "number", optional: true, convert: true },
|
||||
},
|
||||
handler(ctx) {
|
||||
const limit = ctx.params.limit ? Number(ctx.params.limit) : 20;
|
||||
const offset = ctx.params.offset ? Number(ctx.params.offset) : 0;
|
||||
|
||||
let params = {
|
||||
limit,
|
||||
offset,
|
||||
sort: ["-createdAt"],
|
||||
populate: ["author", "favorited", "favoritesCount"],
|
||||
query: {}
|
||||
};
|
||||
let countParams;
|
||||
|
||||
if (ctx.params.tag)
|
||||
params.query.tagList = {"$in" : [ctx.params.tag]};
|
||||
|
||||
return this.Promise.resolve()
|
||||
.then(() => {
|
||||
if (ctx.params.author) {
|
||||
return ctx.call("users.find", { query: { username: ctx.params.author } })
|
||||
.then(users => {
|
||||
if (users.length == 0)
|
||||
return this.Promise.reject(new MoleculerClientError("Author not found"));
|
||||
|
||||
params.query.author = users[0]._id;
|
||||
});
|
||||
}
|
||||
if (ctx.params.favorited) {
|
||||
return ctx.call("users.find", { query: { username: ctx.params.favorited } })
|
||||
.then(users => {
|
||||
if (users.length == 0)
|
||||
return this.Promise.reject(new MoleculerClientError("Author not found"));
|
||||
|
||||
return users[0]._id;
|
||||
})
|
||||
.then(user => {
|
||||
return ctx.call("favorites.find", { fields: ["article"], query: { user }})
|
||||
.then(list => {
|
||||
params.query._id = { $in: list.map(o => o.article) };
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
countParams = Object.assign({}, params);
|
||||
// Remove pagination params
|
||||
if (countParams && countParams.limit)
|
||||
countParams.limit = null;
|
||||
if (countParams && countParams.offset)
|
||||
countParams.offset = null;
|
||||
})
|
||||
.then(() => this.Promise.all([
|
||||
// Get rows
|
||||
this.adapter.find(params),
|
||||
|
||||
// Get count of all rows
|
||||
this.adapter.count(countParams)
|
||||
|
||||
])).then(res => {
|
||||
return this.transformDocuments(ctx, params, res[0])
|
||||
.then(docs => this.transformResult(ctx, docs, ctx.meta.user))
|
||||
.then(r => {
|
||||
r.articlesCount = res[1];
|
||||
return r;
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* List articles from followed authors.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {Number} limit - Pagination limit
|
||||
* @param {Number} offset - Pagination offset
|
||||
*
|
||||
* @returns {Object} List of articles
|
||||
*/
|
||||
feed: {
|
||||
auth: "required",
|
||||
cache: {
|
||||
keys: ["#token", "limit", "offset"]
|
||||
},
|
||||
params: {
|
||||
limit: { type: "number", optional: true, convert: true },
|
||||
offset: { type: "number", optional: true, convert: true },
|
||||
},
|
||||
handler(ctx) {
|
||||
const limit = ctx.params.limit ? Number(ctx.params.limit) : 20;
|
||||
const offset = ctx.params.offset ? Number(ctx.params.offset) : 0;
|
||||
|
||||
let params = {
|
||||
limit,
|
||||
offset,
|
||||
sort: ["-createdAt"],
|
||||
populate: ["author", "favorited", "favoritesCount"],
|
||||
query: {}
|
||||
};
|
||||
let countParams;
|
||||
|
||||
return this.Promise.resolve()
|
||||
.then(() => {
|
||||
return ctx.call("follows.find", { fields: ["follow"], query: { user: ctx.meta.user._id.toString() } })
|
||||
.then(list => {
|
||||
const authors = _.uniq(_.compact(_.flattenDeep(list.map(o => o.follow))));
|
||||
params.query.author = {"$in" : authors};
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
countParams = Object.assign({}, params);
|
||||
// Remove pagination params
|
||||
if (countParams && countParams.limit)
|
||||
countParams.limit = null;
|
||||
if (countParams && countParams.offset)
|
||||
countParams.offset = null;
|
||||
})
|
||||
.then(() => this.Promise.all([
|
||||
// Get rows
|
||||
this.adapter.find(params),
|
||||
|
||||
// Get count of all rows
|
||||
this.adapter.count(countParams)
|
||||
|
||||
])).then(res => {
|
||||
return this.transformDocuments(ctx, params, res[0])
|
||||
.then(docs => this.transformResult(ctx, docs, ctx.meta.user))
|
||||
.then(r => {
|
||||
r.articlesCount = res[1];
|
||||
return r;
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get an article by slug
|
||||
*
|
||||
* @actions
|
||||
* @param {String} id - Article slug
|
||||
*
|
||||
* @returns {Object} Article entity
|
||||
*/
|
||||
get: {
|
||||
cache: {
|
||||
keys: ["#token", "id"]
|
||||
},
|
||||
params: {
|
||||
id: { type: "string" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.findBySlug(ctx.params.id)
|
||||
.then(entity => {
|
||||
if (!entity)
|
||||
return this.Promise.reject(new MoleculerClientError("Article not found!", 404));
|
||||
|
||||
return entity;
|
||||
})
|
||||
.then(doc => this.transformDocuments(ctx, { populate: ["author", "favorited", "favoritesCount"] }, doc))
|
||||
.then(entity => this.transformResult(ctx, entity, ctx.meta.user));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove an article by slug
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} id - Article slug
|
||||
*
|
||||
* @returns {Number} Count of removed articles
|
||||
*/
|
||||
remove: {
|
||||
auth: "required",
|
||||
params: {
|
||||
id: { type: "any" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.findBySlug(ctx.params.id)
|
||||
.then(entity => {
|
||||
if (!entity)
|
||||
return this.Promise.reject(new MoleculerClientError("Article not found!", 404));
|
||||
|
||||
if (entity.author !== ctx.meta.user._id.toString())
|
||||
return this.Promise.reject(new ForbiddenError());
|
||||
|
||||
return this.adapter.removeById(entity._id)
|
||||
.then(() => ctx.call("favorites.removeByArticle", { article: entity._id }))
|
||||
.then(json => this.entityChanged("removed", json, ctx).then(() => json));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Favorite an article
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} id - Article slug
|
||||
*
|
||||
* @returns {Object} Updated article
|
||||
*/
|
||||
favorite: {
|
||||
auth: "required",
|
||||
params: {
|
||||
slug: { type: "string" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.Promise.resolve(ctx.params.slug)
|
||||
.then(slug => this.findBySlug(slug))
|
||||
.then(article => {
|
||||
if (!article)
|
||||
return this.Promise.reject(new MoleculerClientError("Article not found", 404));
|
||||
|
||||
return ctx.call("favorites.add", { article: article._id.toString(), user: ctx.meta.user._id.toString() }).then(() => article);
|
||||
})
|
||||
.then(doc => this.transformDocuments(ctx, { populate: ["author", "favorited", "favoritesCount"] }, doc))
|
||||
.then(entity => this.transformResult(ctx, entity, ctx.meta.user));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Unfavorite an article
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} id - Article slug
|
||||
*
|
||||
* @returns {Object} Updated article
|
||||
*/
|
||||
unfavorite: {
|
||||
auth: "required",
|
||||
params: {
|
||||
slug: { type: "string" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.Promise.resolve(ctx.params.slug)
|
||||
.then(slug => this.findBySlug(slug))
|
||||
.then(article => {
|
||||
if (!article)
|
||||
return this.Promise.reject(new MoleculerClientError("Article not found", 404));
|
||||
|
||||
return ctx.call("favorites.delete", { article: article._id.toString(), user: ctx.meta.user._id.toString() }).then(() => article);
|
||||
})
|
||||
.then(doc => this.transformDocuments(ctx, { populate: ["author", "favorited", "favoritesCount"] }, doc))
|
||||
.then(entity => this.transformResult(ctx, entity, ctx.meta.user));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get list of available tags
|
||||
*
|
||||
* @returns {Object} Tag list
|
||||
*/
|
||||
tags: {
|
||||
cache: {
|
||||
keys: []
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.Promise.resolve()
|
||||
.then(() => this.adapter.find({ fields: ["tagList"], sort: ["createdAt"] }))
|
||||
.then(list => {
|
||||
return _.uniq(_.compact(_.flattenDeep(list.map(o => o.tagList))));
|
||||
})
|
||||
.then(tags => ({ tags }));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all comments of an article.
|
||||
*
|
||||
* @actions
|
||||
* @param {String} slug - Article slug
|
||||
*
|
||||
* @returns {Object} Comment list
|
||||
*
|
||||
*/
|
||||
comments: {
|
||||
cache: {
|
||||
keys: ["#token", "slug"]
|
||||
},
|
||||
params: {
|
||||
slug: { type: "string" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.Promise.resolve(ctx.params.slug)
|
||||
.then(slug => this.findBySlug(slug))
|
||||
.then(article => {
|
||||
if (!article)
|
||||
return this.Promise.reject(new MoleculerClientError("Article not found", 404));
|
||||
|
||||
return ctx.call("comments.list", { article: article._id.toString() });
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new comment to an article.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} slug - Article slug
|
||||
* @param {Object} comment - Comment fields
|
||||
*
|
||||
* @returns {Object} Comment entity
|
||||
*/
|
||||
addComment: {
|
||||
auth: "required",
|
||||
params: {
|
||||
slug: { type: "string" },
|
||||
comment: { type: "object" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.Promise.resolve(ctx.params.slug)
|
||||
.then(slug => this.findBySlug(slug))
|
||||
.then(article => {
|
||||
if (!article)
|
||||
return this.Promise.reject(new MoleculerClientError("Article not found", 404));
|
||||
|
||||
return ctx.call("comments.create", { article: article._id.toString(), comment: ctx.params.comment });
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a comment.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} slug - Article slug
|
||||
* @param {String} commentID - Comment ID
|
||||
* @param {Object} comment - Comment fields
|
||||
*
|
||||
* @returns {Object} Comment entity
|
||||
*/
|
||||
updateComment: {
|
||||
auth: "required",
|
||||
params: {
|
||||
slug: { type: "string" },
|
||||
commentID: { type: "string" },
|
||||
comment: { type: "object" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.Promise.resolve(ctx.params.slug)
|
||||
.then(slug => this.findBySlug(slug))
|
||||
.then(article => {
|
||||
if (!article)
|
||||
return this.Promise.reject(new MoleculerClientError("Article not found", 404));
|
||||
|
||||
return ctx.call("comments.update", { id: ctx.params.commentID, comment: ctx.params.comment });
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a comment.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} slug - Article slug
|
||||
* @param {String} commentID - Comment ID
|
||||
*
|
||||
* @returns {Number} Count of removed comment
|
||||
*/
|
||||
removeComment: {
|
||||
auth: "required",
|
||||
params: {
|
||||
slug: { type: "string" },
|
||||
commentID: { type: "string" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.Promise.resolve(ctx.params.slug)
|
||||
.then(slug => this.findBySlug(slug))
|
||||
.then(article => {
|
||||
if (!article)
|
||||
return this.Promise.reject(new MoleculerClientError("Article not found"));
|
||||
|
||||
return ctx.call("comments.remove", { id: ctx.params.commentID });
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
methods: {
|
||||
/**
|
||||
* Find an article by slug
|
||||
*
|
||||
* @param {String} slug - Article slug
|
||||
*
|
||||
* @results {Object} Promise<Article
|
||||
*/
|
||||
findBySlug(slug) {
|
||||
return this.adapter.findOne({ slug });
|
||||
},
|
||||
|
||||
/**
|
||||
* Transform the result entities to follow the RealWorld API spec
|
||||
*
|
||||
* @param {Context} ctx
|
||||
* @param {Array} entities
|
||||
* @param {Object} user - Logged in user
|
||||
*/
|
||||
transformResult(ctx, entities, user) {
|
||||
if (Array.isArray(entities)) {
|
||||
return this.Promise.map(entities, item => this.transformEntity(ctx, item, user))
|
||||
.then(articles => ({ articles }));
|
||||
} else {
|
||||
return this.transformEntity(ctx, entities, user)
|
||||
.then(article => ({ article }));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Transform a result entity to follow the RealWorld API spec
|
||||
*
|
||||
* @param {Context} ctx
|
||||
* @param {Object} entity
|
||||
* @param {Object} user - Logged in user
|
||||
*/
|
||||
transformEntity(ctx, entity, user) {
|
||||
if (!entity) return this.Promise.resolve();
|
||||
|
||||
return this.Promise.resolve(entity);
|
||||
}
|
||||
},
|
||||
|
||||
events: {
|
||||
"cache.clean.articles"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.users"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.comments"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.follows"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.favorites"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
}
|
||||
}
|
||||
};
|
||||
266
server/services/comments.service.js
Normal file
266
server/services/comments.service.js
Normal file
@@ -0,0 +1,266 @@
|
||||
"use strict";
|
||||
|
||||
const { ForbiddenError } = require("moleculer-web").Errors;
|
||||
const DbService = require("../mixins/db.mixin");
|
||||
|
||||
module.exports = {
|
||||
name: "comments",
|
||||
mixins: [DbService("comments")],
|
||||
|
||||
/**
|
||||
* Default settings
|
||||
*/
|
||||
settings: {
|
||||
fields: ["_id", "author", "article", "body", "createdAt", "updatedAt"],
|
||||
populates: {
|
||||
"author": {
|
||||
action: "users.get",
|
||||
params: {
|
||||
fields: ["_id", "username", "bio", "image"]
|
||||
}
|
||||
}
|
||||
},
|
||||
entityValidator: {
|
||||
article: { type: "string" },
|
||||
body: { type: "string", min: 1 },
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
actions: {
|
||||
/**
|
||||
* Create a comment.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} article - Article ID
|
||||
* @param {Object} comment - Comment entity
|
||||
*
|
||||
* @returns {Object} Created comment entity
|
||||
*/
|
||||
create: {
|
||||
auth: "required",
|
||||
params: {
|
||||
article: { type: "string" },
|
||||
comment: { type: "object" }
|
||||
},
|
||||
handler(ctx) {
|
||||
let entity = ctx.params.comment;
|
||||
entity.article = ctx.params.article;
|
||||
entity.author = ctx.meta.user._id.toString();
|
||||
|
||||
return this.validateEntity(entity)
|
||||
.then(() => {
|
||||
|
||||
entity.createdAt = new Date();
|
||||
entity.updatedAt = new Date();
|
||||
|
||||
return this.adapter.insert(entity)
|
||||
.then(doc => this.transformDocuments(ctx, { populate: ["author"]}, doc))
|
||||
.then(entity => this.transformResult(ctx, entity, ctx.meta.user))
|
||||
.then(json => this.entityChanged("created", json, ctx).then(() => json));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a comment.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} id - Comment ID
|
||||
* @param {Object} comment - Comment modified fields
|
||||
*
|
||||
* @returns {Object} Updated comment entity
|
||||
*/
|
||||
update: {
|
||||
auth: "required",
|
||||
params: {
|
||||
id: { type: "string" },
|
||||
comment: { type: "object", props: {
|
||||
body: { type: "string", min: 1 },
|
||||
} }
|
||||
},
|
||||
handler(ctx) {
|
||||
let newData = ctx.params.comment;
|
||||
newData.updatedAt = new Date();
|
||||
|
||||
return this.getById(ctx.params.id)
|
||||
.then(comment => {
|
||||
if (comment.author !== ctx.meta.user._id.toString())
|
||||
return this.Promise.reject(new ForbiddenError());
|
||||
|
||||
const update = {
|
||||
"$set": newData
|
||||
};
|
||||
|
||||
return this.adapter.updateById(ctx.params.id, update);
|
||||
})
|
||||
.then(doc => this.transformDocuments(ctx, { populate: ["author"]}, doc))
|
||||
.then(entity => this.transformResult(ctx, entity, ctx.meta.user))
|
||||
.then(json => this.entityChanged("updated", json, ctx).then(() => json));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* List of comments by article.
|
||||
*
|
||||
* @actions
|
||||
* @param {String} article - Article ID
|
||||
* @param {Number} limit - Pagination limit
|
||||
* @param {Number} offset - Pagination offset
|
||||
*
|
||||
* @returns {Object} List of comments
|
||||
*/
|
||||
list: {
|
||||
cache: {
|
||||
keys: ["#token", "article", "limit", "offset"]
|
||||
},
|
||||
params: {
|
||||
article: { type: "string" },
|
||||
limit: { type: "number", optional: true, convert: true },
|
||||
offset: { type: "number", optional: true, convert: true },
|
||||
},
|
||||
handler(ctx) {
|
||||
const limit = ctx.params.limit ? Number(ctx.params.limit) : 20;
|
||||
const offset = ctx.params.offset ? Number(ctx.params.offset) : 0;
|
||||
|
||||
let params = {
|
||||
limit,
|
||||
offset,
|
||||
sort: ["-createdAt"],
|
||||
populate: ["author"],
|
||||
query: {
|
||||
article: ctx.params.article
|
||||
}
|
||||
};
|
||||
let countParams;
|
||||
|
||||
return this.Promise.resolve()
|
||||
.then(() => {
|
||||
countParams = Object.assign({}, params);
|
||||
// Remove pagination params
|
||||
if (countParams && countParams.limit)
|
||||
countParams.limit = null;
|
||||
if (countParams && countParams.offset)
|
||||
countParams.offset = null;
|
||||
})
|
||||
.then(() => this.Promise.all([
|
||||
// Get rows
|
||||
this.adapter.find(params),
|
||||
|
||||
// Get count of all rows
|
||||
this.adapter.count(countParams)
|
||||
|
||||
])).then(res => {
|
||||
return this.transformDocuments(ctx, params, res[0])
|
||||
.then(docs => this.transformResult(ctx, docs, ctx.meta.user))
|
||||
.then(r => {
|
||||
r.commentsCount = res[1];
|
||||
return r;
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a comment
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} id - Comment ID
|
||||
*
|
||||
* @returns {Number} Count of removed comments
|
||||
*/
|
||||
remove: {
|
||||
auth: "required",
|
||||
params: {
|
||||
id: { type: "any" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.getById(ctx.params.id)
|
||||
.then(comment => {
|
||||
if (comment.author !== ctx.meta.user._id.toString())
|
||||
return this.Promise.reject(new ForbiddenError());
|
||||
|
||||
return this.adapter.removeById(ctx.params.id)
|
||||
.then(json => this.entityChanged("updated", json, ctx).then(() => json));
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
methods: {
|
||||
|
||||
/**
|
||||
* Transform the result entities to follow the RealWorld API spec
|
||||
*
|
||||
* @param {Context} ctx
|
||||
* @param {Array} entities
|
||||
* @param {Object} user - Logged in user
|
||||
*/
|
||||
transformResult(ctx, entities, user) {
|
||||
if (Array.isArray(entities)) {
|
||||
return this.Promise.map(entities, item => this.transformEntity(ctx, item, user))
|
||||
.then(comments => ({ comments }));
|
||||
} else {
|
||||
return this.transformEntity(ctx, entities, user)
|
||||
.then(comment => ({ comment }));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Transform a result entity to follow the RealWorld API spec
|
||||
*
|
||||
* @param {Context} ctx
|
||||
* @param {Object} entity
|
||||
* @param {Object} user - Logged in user
|
||||
*/
|
||||
transformEntity(ctx, entity, loggedInUser) {
|
||||
if (!entity) return this.Promise.resolve();
|
||||
|
||||
return this.Promise.resolve(entity)
|
||||
.then(entity => {
|
||||
entity.id = entity._id;
|
||||
|
||||
if (loggedInUser) {
|
||||
return ctx.call("follows.has", { user: loggedInUser._id.toString(), follow: entity.author._id })
|
||||
.then(res => {
|
||||
entity.author.following = res;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
entity.author.following = false;
|
||||
|
||||
return entity;
|
||||
});
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
events: {
|
||||
"cache.clean.comments"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.users"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.follows"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.articles"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
}
|
||||
}
|
||||
};
|
||||
177
server/services/favorites.service.js
Normal file
177
server/services/favorites.service.js
Normal file
@@ -0,0 +1,177 @@
|
||||
"use strict";
|
||||
|
||||
const { MoleculerClientError } = require("moleculer").Errors;
|
||||
const DbService = require("../mixins/db.mixin");
|
||||
|
||||
|
||||
module.exports = {
|
||||
name: "favorites",
|
||||
mixins: [DbService("favorites")],
|
||||
|
||||
/**
|
||||
* Default settings
|
||||
*/
|
||||
settings: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
actions: {
|
||||
|
||||
/**
|
||||
* Create a new favorite record
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} article - Article ID
|
||||
* @param {String} user - User ID
|
||||
* @returns {Object} Created favorite record
|
||||
*/
|
||||
add: {
|
||||
params: {
|
||||
article: { type: "string" },
|
||||
user: { type: "string" },
|
||||
},
|
||||
handler(ctx) {
|
||||
const { article, user } = ctx.params;
|
||||
return this.findByArticleAndUser(article, user)
|
||||
.then(item => {
|
||||
if (item)
|
||||
return this.Promise.reject(new MoleculerClientError("Articles has already favorited"));
|
||||
|
||||
return this.adapter.insert({ article, user, createdAt: new Date() })
|
||||
.then(json => this.entityChanged("created", json, ctx).then(() => json));
|
||||
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the given 'article' is followed by 'user'.
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} article - Article ID
|
||||
* @param {String} user - User ID
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
has: {
|
||||
cache: {
|
||||
keys: ["article", "user"]
|
||||
},
|
||||
params: {
|
||||
article: { type: "string" },
|
||||
user: { type: "string" },
|
||||
},
|
||||
handler(ctx) {
|
||||
const { article, user } = ctx.params;
|
||||
return this.findByArticleAndUser(article, user)
|
||||
.then(item => !!item);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Count of favorites.
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String?} article - Article ID
|
||||
* @param {String?} user - User ID
|
||||
* @returns {Number}
|
||||
*/
|
||||
count: {
|
||||
cache: {
|
||||
keys: ["article", "user"]
|
||||
},
|
||||
params: {
|
||||
article: { type: "string", optional: true },
|
||||
user: { type: "string", optional: true },
|
||||
},
|
||||
handler(ctx) {
|
||||
let query = {};
|
||||
if (ctx.params.article)
|
||||
query = { article: ctx.params.article };
|
||||
|
||||
if (ctx.params.user)
|
||||
query = { user: ctx.params.user };
|
||||
|
||||
return this.adapter.count({ query });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a favorite record
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} article - Article ID
|
||||
* @param {String} user - User ID
|
||||
* @returns {Number} Count of removed records
|
||||
*/
|
||||
delete: {
|
||||
params: {
|
||||
article: { type: "string" },
|
||||
user: { type: "string" },
|
||||
},
|
||||
handler(ctx) {
|
||||
const { article, user } = ctx.params;
|
||||
return this.findByArticleAndUser(article, user)
|
||||
.then(item => {
|
||||
if (!item)
|
||||
return this.Promise.reject(new MoleculerClientError("Articles has not favorited yet"));
|
||||
|
||||
return this.adapter.removeById(item._id)
|
||||
.then(json => this.entityChanged("removed", json, ctx).then(() => json));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove all favorites by article
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} article - Article ID
|
||||
* @returns {Number} Count of removed records
|
||||
*/
|
||||
removeByArticle: {
|
||||
params: {
|
||||
article: { type: "string" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.adapter.removeMany(ctx.params);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
methods: {
|
||||
/**
|
||||
* Find the first favorite record by 'article' or 'user'
|
||||
* @param {String} article - Article ID
|
||||
* @param {String} user - User ID
|
||||
*/
|
||||
findByArticleAndUser(article, user) {
|
||||
return this.adapter.findOne({ article, user });
|
||||
},
|
||||
},
|
||||
|
||||
events: {
|
||||
"cache.clean.favorites"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.users"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.articles"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
}
|
||||
}
|
||||
};
|
||||
153
server/services/follows.service.js
Normal file
153
server/services/follows.service.js
Normal file
@@ -0,0 +1,153 @@
|
||||
"use strict";
|
||||
|
||||
const { MoleculerClientError } = require("moleculer").Errors;
|
||||
const DbService = require("../mixins/db.mixin");
|
||||
|
||||
module.exports = {
|
||||
name: "follows",
|
||||
mixins: [DbService("follows")],
|
||||
|
||||
/**
|
||||
* Default settings
|
||||
*/
|
||||
settings: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
actions: {
|
||||
|
||||
/**
|
||||
* Create a new following record
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} user - Follower username
|
||||
* @param {String} follow - Followee username
|
||||
* @returns {Object} Created following record
|
||||
*/
|
||||
add: {
|
||||
params: {
|
||||
user: { type: "string" },
|
||||
follow: { type: "string" },
|
||||
},
|
||||
handler(ctx) {
|
||||
const { follow, user } = ctx.params;
|
||||
return this.findByFollowAndUser(follow, user)
|
||||
.then(item => {
|
||||
if (item)
|
||||
return this.Promise.reject(new MoleculerClientError("User has already followed"));
|
||||
|
||||
return this.adapter.insert({ follow, user, createdAt: new Date() })
|
||||
.then(json => this.entityChanged("created", json, ctx).then(() => json));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the given 'follow' user is followed by 'user' user.
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} user - Follower username
|
||||
* @param {String} follow - Followee username
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
has: {
|
||||
cache: {
|
||||
keys: ["article", "user"]
|
||||
},
|
||||
params: {
|
||||
user: { type: "string" },
|
||||
follow: { type: "string" },
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.findByFollowAndUser(ctx.params.follow, ctx.params.user)
|
||||
.then(item => !!item);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Count of following.
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String?} user - Follower username
|
||||
* @param {String?} follow - Followee username
|
||||
* @returns {Number}
|
||||
*/
|
||||
count: {
|
||||
cache: {
|
||||
keys: ["article", "user"]
|
||||
},
|
||||
params: {
|
||||
follow: { type: "string", optional: true },
|
||||
user: { type: "string", optional: true },
|
||||
},
|
||||
handler(ctx) {
|
||||
let query = {};
|
||||
if (ctx.params.follow)
|
||||
query = { follow: ctx.params.follow };
|
||||
|
||||
if (ctx.params.user)
|
||||
query = { user: ctx.params.user };
|
||||
|
||||
return this.adapter.count({ query });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a following record
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} user - Follower username
|
||||
* @param {String} follow - Followee username
|
||||
* @returns {Number} Count of removed records
|
||||
*/
|
||||
delete: {
|
||||
params: {
|
||||
user: { type: "string" },
|
||||
follow: { type: "string" },
|
||||
},
|
||||
handler(ctx) {
|
||||
const { follow, user } = ctx.params;
|
||||
return this.findByFollowAndUser(follow, user)
|
||||
.then(item => {
|
||||
if (!item)
|
||||
return this.Promise.reject(new MoleculerClientError("User has not followed yet"));
|
||||
|
||||
return this.adapter.removeById(item._id)
|
||||
.then(json => this.entityChanged("removed", json, ctx).then(() => json));
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
methods: {
|
||||
/**
|
||||
* Find the first following record by 'follow' or 'user'
|
||||
* @param {String} follow - Followee username
|
||||
* @param {String} user - Follower username
|
||||
*/
|
||||
findByFollowAndUser(follow, user) {
|
||||
return this.adapter.findOne({ follow, user });
|
||||
},
|
||||
},
|
||||
|
||||
events: {
|
||||
"cache.clean.follows"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.users"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
}
|
||||
}
|
||||
};
|
||||
96
server/services/metrics.service.js
Normal file
96
server/services/metrics.service.js
Normal file
@@ -0,0 +1,96 @@
|
||||
let _ = require("lodash");
|
||||
|
||||
module.exports = {
|
||||
name: "metrics",
|
||||
|
||||
events: {
|
||||
"metrics.trace.span.start"(payload) {
|
||||
this.requests[payload.id] = payload;
|
||||
payload.spans = [];
|
||||
|
||||
if (payload.parent) {
|
||||
let parent = this.requests[payload.parent];
|
||||
if (parent)
|
||||
parent.spans.push(payload.id);
|
||||
}
|
||||
},
|
||||
|
||||
"metrics.trace.span.finish"(payload) {
|
||||
let item = this.requests[payload.id];
|
||||
_.assign(item, payload);
|
||||
|
||||
if (!payload.parent)
|
||||
this.printRequest(payload.id);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
printRequest(id) {
|
||||
let main = this.requests[id];
|
||||
|
||||
let w = 73;
|
||||
let r = _.repeat;
|
||||
let gw = 35;
|
||||
let maxTitle = w - 2 - 2 - gw - 2 - 1;
|
||||
|
||||
this.logger.info(["┌", r("─", w-2), "┐"].join(""));
|
||||
|
||||
let printSpanTime = (span) => {
|
||||
let time = span.duration.toFixed(2);
|
||||
|
||||
let maxActionName = maxTitle - (span.level-1) * 2 - time.length - 3 - (span.fromCache ? 2 : 0) - (span.remoteCall ? 2 : 0) - (span.error ? 2 : 0);
|
||||
let actionName = span.action ? span.action.name : "";
|
||||
if (actionName.length > maxActionName)
|
||||
actionName = _.truncate(span.action.name, { length: maxActionName });
|
||||
|
||||
let strAction = [
|
||||
r(" ", span.level - 1),
|
||||
actionName,
|
||||
r(" ", maxActionName - actionName.length + 1),
|
||||
span.fromCache ? "* " : "",
|
||||
span.remoteCall ? "» " : "",
|
||||
span.error ? "× " : "",
|
||||
time,
|
||||
"ms "
|
||||
].join("");
|
||||
|
||||
if (span.startTime == null || span.endTime == null) {
|
||||
this.logger.info(strAction + "! Missing invoke !");
|
||||
return;
|
||||
}
|
||||
|
||||
let gstart = (span.startTime - main.startTime) / (main.endTime - main.startTime) * 100;
|
||||
let gstop = (span.endTime - main.startTime) / (main.endTime - main.startTime) * 100;
|
||||
|
||||
if (_.isNaN(gstart) && _.isNaN(gstop)) {
|
||||
gstart = 0;
|
||||
gstop = 100;
|
||||
}
|
||||
|
||||
let p1 = Math.round(gw * gstart / 100);
|
||||
let p2 = Math.round(gw * gstop / 100) - p1;
|
||||
let p3 = Math.max(gw - (p1 + p2), 0);
|
||||
|
||||
let gauge = [
|
||||
"[",
|
||||
r(".", p1),
|
||||
r("■", p2),
|
||||
r(".", p3),
|
||||
"]"
|
||||
].join("");
|
||||
|
||||
this.logger.info("│ " + strAction + gauge + " │");
|
||||
|
||||
if (span.spans.length > 0)
|
||||
span.spans.forEach(spanID => printSpanTime(this.requests[spanID]));
|
||||
};
|
||||
|
||||
printSpanTime(main);
|
||||
this.logger.info(["└", r("─", w-2), "┘"].join(""));
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.requests = {};
|
||||
}
|
||||
};
|
||||
390
server/services/users.service.js
Normal file
390
server/services/users.service.js
Normal file
@@ -0,0 +1,390 @@
|
||||
"use strict";
|
||||
|
||||
const { MoleculerClientError } = require("moleculer").Errors;
|
||||
|
||||
//const crypto = require("crypto");
|
||||
const bcrypt = require("bcrypt");
|
||||
const jwt = require("jsonwebtoken");
|
||||
|
||||
const DbService = require("../mixins/db.mixin");
|
||||
|
||||
module.exports = {
|
||||
name: "users",
|
||||
mixins: [DbService("users")],
|
||||
|
||||
/**
|
||||
* Default settings
|
||||
*/
|
||||
settings: {
|
||||
/** Secret for JWT */
|
||||
JWT_SECRET: process.env.JWT_SECRET || "jwt-conduit-secret",
|
||||
|
||||
/** Public fields */
|
||||
fields: ["_id", "username", "email", "bio", "image"],
|
||||
|
||||
/** Validator schema for entity */
|
||||
entityValidator: {
|
||||
username: { type: "string", min: 2, pattern: /^[a-zA-Z0-9]+$/ },
|
||||
password: { type: "string", min: 6 },
|
||||
email: { type: "email" },
|
||||
bio: { type: "string", optional: true },
|
||||
image: { type: "string", optional: true },
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
actions: {
|
||||
/**
|
||||
* Register a new user
|
||||
*
|
||||
* @actions
|
||||
* @param {Object} user - User entity
|
||||
*
|
||||
* @returns {Object} Created entity & token
|
||||
*/
|
||||
create: {
|
||||
params: {
|
||||
user: { type: "object" }
|
||||
},
|
||||
handler(ctx) {
|
||||
let entity = ctx.params.user;
|
||||
return this.validateEntity(entity)
|
||||
.then(() => {
|
||||
if (entity.username)
|
||||
return this.adapter.findOne({ username: entity.username })
|
||||
.then(found => {
|
||||
if (found)
|
||||
return Promise.reject(new MoleculerClientError("Username is exist!", 422, "", [{ field: "username", message: "is exist"}]));
|
||||
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
if (entity.email)
|
||||
return this.adapter.findOne({ email: entity.email })
|
||||
.then(found => {
|
||||
if (found)
|
||||
return Promise.reject(new MoleculerClientError("Email is exist!", 422, "", [{ field: "email", message: "is exist"}]));
|
||||
});
|
||||
|
||||
})
|
||||
.then(() => {
|
||||
entity.password = bcrypt.hashSync(entity.password, 10);
|
||||
entity.bio = entity.bio || "";
|
||||
entity.image = entity.image || null;
|
||||
entity.createdAt = new Date();
|
||||
|
||||
return this.adapter.insert(entity)
|
||||
.then(doc => this.transformDocuments(ctx, {}, doc))
|
||||
.then(user => this.transformEntity(user, true, ctx.meta.token))
|
||||
.then(json => this.entityChanged("created", json, ctx).then(() => json));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Login with username & password
|
||||
*
|
||||
* @actions
|
||||
* @param {Object} user - User credentials
|
||||
*
|
||||
* @returns {Object} Logged in user with token
|
||||
*/
|
||||
login: {
|
||||
params: {
|
||||
user: { type: "object", props: {
|
||||
email: { type: "email" },
|
||||
password: { type: "string", min: 1 }
|
||||
}}
|
||||
},
|
||||
handler(ctx) {
|
||||
const { email, password } = ctx.params.user;
|
||||
|
||||
return this.Promise.resolve()
|
||||
.then(() => this.adapter.findOne({ email }))
|
||||
.then(user => {
|
||||
if (!user)
|
||||
return this.Promise.reject(new MoleculerClientError("Email or password is invalid!", 422, "", [{ field: "email", message: "is not found"}]));
|
||||
|
||||
return bcrypt.compare(password, user.password).then(res => {
|
||||
if (!res)
|
||||
return Promise.reject(new MoleculerClientError("Wrong password!", 422, "", [{ field: "email", message: "is not found"}]));
|
||||
|
||||
// Transform user entity (remove password and all protected fields)
|
||||
return this.transformDocuments(ctx, {}, user);
|
||||
});
|
||||
})
|
||||
.then(user => this.transformEntity(user, true, ctx.meta.token));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get user by JWT token (for API GW authentication)
|
||||
*
|
||||
* @actions
|
||||
* @param {String} token - JWT token
|
||||
*
|
||||
* @returns {Object} Resolved user
|
||||
*/
|
||||
resolveToken: {
|
||||
cache: {
|
||||
keys: ["token"],
|
||||
ttl: 60 * 60 // 1 hour
|
||||
},
|
||||
params: {
|
||||
token: "string"
|
||||
},
|
||||
handler(ctx) {
|
||||
return new this.Promise((resolve, reject) => {
|
||||
jwt.verify(ctx.params.token, this.settings.JWT_SECRET, (err, decoded) => {
|
||||
if (err)
|
||||
return reject(err);
|
||||
|
||||
resolve(decoded);
|
||||
});
|
||||
|
||||
})
|
||||
.then(decoded => {
|
||||
if (decoded.id)
|
||||
return this.getById(decoded.id);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current user entity.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @returns {Object} User entity
|
||||
*/
|
||||
me: {
|
||||
auth: "required",
|
||||
cache: {
|
||||
keys: ["#token"]
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.getById(ctx.meta.user._id)
|
||||
.then(user => {
|
||||
if (!user)
|
||||
return this.Promise.reject(new MoleculerClientError("User not found!", 400));
|
||||
|
||||
return this.transformDocuments(ctx, {}, user);
|
||||
})
|
||||
.then(user => this.transformEntity(user, true, ctx.meta.token));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update current user entity.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {Object} user - Modified fields
|
||||
* @returns {Object} User entity
|
||||
*/
|
||||
updateMyself: {
|
||||
auth: "required",
|
||||
params: {
|
||||
user: { type: "object", props: {
|
||||
username: { type: "string", min: 2, optional: true, pattern: /^[a-zA-Z0-9]+$/ },
|
||||
password: { type: "string", min: 6, optional: true },
|
||||
email: { type: "email", optional: true },
|
||||
bio: { type: "string", optional: true },
|
||||
image: { type: "string", optional: true },
|
||||
}}
|
||||
},
|
||||
handler(ctx) {
|
||||
const newData = ctx.params.user;
|
||||
return this.Promise.resolve()
|
||||
.then(() => {
|
||||
if (newData.username)
|
||||
return this.adapter.findOne({ username: newData.username })
|
||||
.then(found => {
|
||||
if (found && found._id.toString() !== ctx.meta.user._id.toString())
|
||||
return Promise.reject(new MoleculerClientError("Username is exist!", 422, "", [{ field: "username", message: "is exist"}]));
|
||||
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
if (newData.email)
|
||||
return this.adapter.findOne({ email: newData.email })
|
||||
.then(found => {
|
||||
if (found && found._id.toString() !== ctx.meta.user._id.toString())
|
||||
return Promise.reject(new MoleculerClientError("Email is exist!", 422, "", [{ field: "email", message: "is exist"}]));
|
||||
});
|
||||
|
||||
})
|
||||
.then(() => {
|
||||
newData.updatedAt = new Date();
|
||||
const update = {
|
||||
"$set": newData
|
||||
};
|
||||
return this.adapter.updateById(ctx.meta.user._id, update);
|
||||
})
|
||||
.then(doc => this.transformDocuments(ctx, {}, doc))
|
||||
.then(user => this.transformEntity(user, true, ctx.meta.token))
|
||||
.then(json => this.entityChanged("updated", json, ctx).then(() => json));
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a user profile.
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} username - Username
|
||||
* @returns {Object} User entity
|
||||
*/
|
||||
profile: {
|
||||
cache: {
|
||||
keys: ["#token", "username"]
|
||||
},
|
||||
params: {
|
||||
username: { type: "string" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.adapter.findOne({ username: ctx.params.username })
|
||||
.then(user => {
|
||||
if (!user)
|
||||
return this.Promise.reject(new MoleculerClientError("User not found!", 404));
|
||||
|
||||
return this.transformDocuments(ctx, {}, user);
|
||||
})
|
||||
.then(user => this.transformProfile(ctx, user, ctx.meta.user));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Follow a user
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} username - Followed username
|
||||
* @returns {Object} Current user entity
|
||||
*/
|
||||
follow: {
|
||||
auth: "required",
|
||||
params: {
|
||||
username: { type: "string" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.adapter.findOne({ username: ctx.params.username })
|
||||
.then(user => {
|
||||
if (!user)
|
||||
return this.Promise.reject(new MoleculerClientError("User not found!", 404));
|
||||
|
||||
return ctx.call("follows.add", { user: ctx.meta.user._id.toString(), follow: user._id.toString() })
|
||||
.then(() => this.transformDocuments(ctx, {}, user));
|
||||
})
|
||||
.then(user => this.transformProfile(ctx, user, ctx.meta.user));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Unfollow a user
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} username - Unfollowed username
|
||||
* @returns {Object} Current user entity
|
||||
*/
|
||||
unfollow: {
|
||||
auth: "required",
|
||||
params: {
|
||||
username: { type: "string" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.adapter.findOne({ username: ctx.params.username })
|
||||
.then(user => {
|
||||
if (!user)
|
||||
return this.Promise.reject(new MoleculerClientError("User not found!", 404));
|
||||
|
||||
return ctx.call("follows.delete", { user: ctx.meta.user._id.toString(), follow: user._id.toString() })
|
||||
.then(() => this.transformDocuments(ctx, {}, user));
|
||||
})
|
||||
.then(user => this.transformProfile(ctx, user, ctx.meta.user));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
methods: {
|
||||
/**
|
||||
* Generate a JWT token from user entity
|
||||
*
|
||||
* @param {Object} user
|
||||
*/
|
||||
generateJWT(user) {
|
||||
const today = new Date();
|
||||
const exp = new Date(today);
|
||||
exp.setDate(today.getDate() + 60);
|
||||
|
||||
return jwt.sign({
|
||||
id: user._id,
|
||||
username: user.username,
|
||||
exp: Math.floor(exp.getTime() / 1000)
|
||||
}, this.settings.JWT_SECRET);
|
||||
},
|
||||
|
||||
/**
|
||||
* Transform returned user entity. Generate JWT token if neccessary.
|
||||
*
|
||||
* @param {Object} user
|
||||
* @param {Boolean} withToken
|
||||
*/
|
||||
transformEntity(user, withToken, token) {
|
||||
if (user) {
|
||||
//user.image = user.image || "https://www.gravatar.com/avatar/" + crypto.createHash("md5").update(user.email).digest("hex") + "?d=robohash";
|
||||
user.image = user.image || "";
|
||||
if (withToken)
|
||||
user.token = token || this.generateJWT(user);
|
||||
}
|
||||
|
||||
return { user };
|
||||
},
|
||||
|
||||
/**
|
||||
* Transform returned user entity as profile.
|
||||
*
|
||||
* @param {Context} ctx
|
||||
* @param {Object} user
|
||||
* @param {Object?} loggedInUser
|
||||
*/
|
||||
transformProfile(ctx, user, loggedInUser) {
|
||||
//user.image = user.image || "https://www.gravatar.com/avatar/" + crypto.createHash("md5").update(user.email).digest("hex") + "?d=robohash";
|
||||
user.image = user.image || "https://static.productionready.io/images/smiley-cyrus.jpg";
|
||||
|
||||
if (loggedInUser) {
|
||||
return ctx.call("follows.has", { user: loggedInUser._id.toString(), follow: user._id.toString() })
|
||||
.then(res => {
|
||||
user.following = res;
|
||||
return { profile: user };
|
||||
});
|
||||
}
|
||||
|
||||
user.following = false;
|
||||
|
||||
return { profile: user };
|
||||
}
|
||||
},
|
||||
|
||||
events: {
|
||||
"cache.clean.users"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.follows"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user