├── logo.png ├── public ├── favicon.ico └── index.html ├── config ├── production.json └── default.json ├── src ├── middleware │ ├── index.js │ ├── tag-response.js │ ├── user-createlogin.js │ └── article-response.js ├── models │ ├── tags.model.js │ ├── articlecomments.model.js │ ├── articles.model.js │ └── users.model.js ├── hooks │ ├── article-delete-response.js │ ├── errors-response.js │ ├── hide-method.js │ ├── authenticateif.js │ ├── extract-token.js │ ├── article-get.js │ ├── authenticate.js │ ├── process-user.js │ ├── tag-find.js │ ├── users-response.js │ ├── authenticate-response.js │ ├── user-response.js │ ├── log.js │ ├── follow-response.js │ ├── article-create.js │ ├── article-update.js │ ├── profile-response.js │ ├── article-delete.js │ ├── article-find.js │ └── comments-response.js ├── logger.js ├── services │ ├── profiles │ │ ├── profiles.class.js │ │ ├── profiles.service.js │ │ ├── follow │ │ │ ├── follow.hooks.js │ │ │ ├── follow.service.js │ │ │ └── follow.class.js │ │ └── profiles.hooks.js │ ├── user │ │ ├── user.service.js │ │ ├── user.class.js │ │ └── user.hooks.js │ ├── articles │ │ ├── feed │ │ │ ├── feed.hooks.js │ │ │ ├── feed.service.js │ │ │ └── feed.class.js │ │ ├── favorite │ │ │ ├── favorite.hooks.js │ │ │ ├── favorite.service.js │ │ │ └── favorite.class.js │ │ ├── articles.service.js │ │ ├── comments │ │ │ ├── comments.hooks.js │ │ │ ├── comments.service.js │ │ │ └── comments.class.js │ │ └── articles.hooks.js │ ├── tags │ │ ├── tags.hooks.js │ │ └── tags.service.js │ ├── articlecomments │ │ ├── articlecomments.hooks.js │ │ └── articlecomments.service.js │ ├── users │ │ ├── users.service.js │ │ └── users.hooks.js │ └── index.js ├── index.js ├── app.hooks.js ├── authentication.js ├── app.js ├── channels.js └── common │ └── helpers.js ├── .gitignore ├── .travis.yml ├── test ├── services │ ├── follow.test.js │ ├── articles │ │ └── feed.test.js │ ├── comments.test.js │ ├── favorite.test.js │ ├── articlecomments.test.js │ ├── profiles.test.js │ ├── user.test.js │ ├── users.test.js │ ├── articles.test.js │ ├── tags.test.js │ └── client-articles.test.js ├── hooks │ ├── tag-find.test.js │ ├── article-find.test.js │ ├── authenticate.test.js │ ├── extract-token.test.js │ ├── process-user.test.js │ ├── user-response.test.js │ ├── users-response.test.js │ ├── errors-response.test.js │ ├── follow-response.test.js │ ├── authenticate-response.test.js │ ├── article-get.test.js │ ├── article-delete-response.test.js │ ├── article-delete.test.js │ ├── article-update.test.js │ ├── comments-response.test.js │ ├── profile-response.test.js │ └── article-create.test.js └── app.test.js ├── .eslintrc.json ├── LICENSE ├── package.json └── readme.md /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randyscotsmithey/feathers-realworld-example-app/HEAD/logo.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randyscotsmithey/feathers-realworld-example-app/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "real-world-feathers-app.feathersjs.com", 3 | "port": "PORT", 4 | "paginate": { 5 | "default": 10, 6 | "max": 50 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/middleware/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | module.exports = function (app) { 3 | // Add your custom middleware here. Remember that 4 | // in Express, the order matters. 5 | }; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /bower_components 6 | 7 | # IDEs and editors 8 | /.idea 9 | .project 10 | .classpath 11 | *.launch 12 | .settings/ 13 | 14 | 15 | #System Files 16 | .DS_Store 17 | Thumbs.db 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "lts/*" 4 | 5 | services: mongodb 6 | 7 | before_script: 8 | - sleep 15 9 | - mongo mydb_test --eval 'db.createUser({user:"travis",pwd:"test",roles:["readWrite"]});' 10 | - export MONGODB_FEATHERS_REALWORLD="mongodb://travis:test@127.0.0.1:27017/mydb_test" 11 | -------------------------------------------------------------------------------- /test/services/follow.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('../../src/app'); 3 | 4 | describe('\'follow\' service', () => { 5 | it('registered the service', () => { 6 | const service = app.service('profiles/:username/follow'); 7 | 8 | assert.ok(service, 'Registered the service'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/services/articles/feed.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('../../../src/app'); 3 | 4 | describe('\'articles/feed\' service', () => { 5 | it('registered the service', () => { 6 | const service = app.service('articles/feed'); 7 | 8 | assert.ok(service, 'Registered the service'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/services/comments.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('../../src/app'); 3 | 4 | describe('\'comments\' service', () => { 5 | it('registered the service', () => { 6 | const service = app.service('articles/:slug/comments'); 7 | 8 | assert.ok(service, 'Registered the service'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/services/favorite.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('../../src/app'); 3 | 4 | describe('\'favorite\' service', () => { 5 | it('registered the service', () => { 6 | const service = app.service('articles/:slug/favorite'); 7 | 8 | assert.ok(service, 'Registered the service'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/services/articlecomments.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('../../src/app'); 3 | 4 | describe('\'articlecomments\' service', () => { 5 | it('registered the service', () => { 6 | const service = app.service('articlecomments'); 7 | 8 | assert.ok(service, 'Registered the service'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/models/tags.model.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | const mongoose = require('mongoose'); 3 | 4 | module.exports = function (app) { 5 | const Schema = mongoose.Schema; 6 | const TagsSchema = new Schema({ 7 | name: { type: String, index: true, unique: true }, 8 | popularity: {type: Number, default: 1} 9 | }); 10 | 11 | const Model = mongoose.model('tags', TagsSchema); 12 | return Model; 13 | }; 14 | -------------------------------------------------------------------------------- /src/hooks/article-delete-response.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | module.exports = function (options = {}) { 6 | return async context => { 7 | context.app.service('articlecomments').remove(null,{query: {articleId: context.id}}); 8 | return context; 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /src/hooks/errors-response.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | module.exports = function (options = {}) { 6 | return async context => { 7 | 8 | context.error.errors = {body: [{name: context.error.name, code: context.error.code, message: context.error.message}]}; 9 | return context; 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | const { createLogger, format, transports } = require('winston'); 2 | 3 | // Configure the Winston logger. For the complete documentation see https://github.com/winstonjs/winston 4 | const logger = createLogger({ 5 | // To see more detailed errors, change this to 'debug' 6 | level: 'info', 7 | format: format.combine( 8 | format.splat(), 9 | format.simple() 10 | ), 11 | transports: [ 12 | new transports.Console() 13 | ], 14 | }); 15 | 16 | module.exports = logger; 17 | -------------------------------------------------------------------------------- /src/models/articlecomments.model.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | const mongoose = require('mongoose'); 3 | 4 | module.exports = function (app) { 5 | const Schema = mongoose.Schema; 6 | const CommentsSchema = new Schema({ 7 | body: String, 8 | articleId: Schema.Types.ObjectId, 9 | userId: Schema.Types.ObjectId, 10 | id: Number 11 | }, 12 | { 13 | timestamps: true 14 | }); 15 | const Model = mongoose.model('articlecomments', CommentsSchema); 16 | return Model; 17 | }; 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 2017 9 | }, 10 | "extends": "eslint:recommended", 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 2 15 | ], 16 | "linebreak-style": [ 17 | "error", 18 | "unix" 19 | ], 20 | "quotes": [ 21 | "error", 22 | "single" 23 | ], 24 | "semi": [ 25 | "error", 26 | "always" 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/services/profiles/profiles.class.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | class Service { 3 | constructor (options) { 4 | this.options = options || {}; 5 | } 6 | 7 | setup(app) { 8 | this.app = (app); 9 | } 10 | 11 | async get (id, params) { 12 | 13 | return this.app.service('users').find({ 14 | query: { 15 | username: id 16 | } 17 | }); 18 | 19 | } 20 | } 21 | module.exports = function (options) { 22 | return new Service(options); 23 | }; 24 | 25 | module.exports.Service = Service; 26 | -------------------------------------------------------------------------------- /src/hooks/hide-method.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | const errors = require('@feathersjs/errors'); 4 | 5 | // eslint-disable-next-line no-unused-vars 6 | module.exports = function (options = {}) { 7 | return context => { 8 | if (context.params.provider && (context.params.provider == 'external' || context.params.provider == 'rest')) { 9 | throw new errors.MethodNotAllowed('Method not allowed'); 10 | } 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /src/middleware/tag-response.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | module.exports = async function (req,res,next) { 6 | 7 | if (res.data.data && res.data.data.length > 0) { 8 | let tagList = []; 9 | res.data.data.forEach(function(tag) { 10 | tagList.push(tag.name); 11 | }); 12 | res.data = {tags: tagList}; 13 | } else { 14 | res.data = {tags: []}; 15 | } 16 | 17 | next(); 18 | }; 19 | -------------------------------------------------------------------------------- /src/hooks/authenticateif.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | 4 | const { authenticate } = require('@feathersjs/authentication').hooks; 5 | 6 | // eslint-disable-next-line no-unused-vars 7 | module.exports = function (options = {}) { 8 | return async context => { 9 | if (context.params.headers && context.params.headers.authorization) { 10 | let doauth = authenticate('jwt'); 11 | return doauth(context); 12 | } 13 | return context; 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /src/middleware/user-createlogin.js: -------------------------------------------------------------------------------- 1 | 2 | // eslint-disable-next-line no-unused-vars 3 | module.exports = async function (req,res,next) { 4 | 5 | if (req.method === 'POST') { 6 | let authors = await res.hook.app.service('users/login').create({ 7 | user: { 8 | strategy: 'local', 9 | email: req.body.user.email, 10 | password: req.body.user.password 11 | } 12 | },{ 13 | query: {}, 14 | route: {}, 15 | provider: 'rest', 16 | headers: req.headers 17 | }); 18 | res.data.user.token = authors.user.token; 19 | } 20 | next(); 21 | }; 22 | -------------------------------------------------------------------------------- /src/services/user/user.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `user` service on path `/user` 2 | const createService = require('./user.class.js'); 3 | const hooks = require('./user.hooks'); 4 | 5 | module.exports = function (app) { 6 | 7 | const paginate = app.get('paginate'); 8 | 9 | const options = { 10 | paginate 11 | }; 12 | 13 | // Initialize our service with any options it requires 14 | app.use('/user', createService(options)); 15 | 16 | // Get our initialized service so that we can register hooks 17 | const service = app.service('user'); 18 | service.setup(app); 19 | 20 | service.hooks(hooks); 21 | }; 22 | -------------------------------------------------------------------------------- /src/services/articles/feed/feed.hooks.js: -------------------------------------------------------------------------------- 1 | const { authenticate } = require('@feathersjs/authentication').hooks; 2 | 3 | module.exports = { 4 | before: { 5 | all: [ authenticate('jwt')], 6 | find: [], 7 | get: [], 8 | create: [], 9 | update: [], 10 | patch: [], 11 | remove: [] 12 | }, 13 | 14 | after: { 15 | all: [], 16 | find: [], 17 | get: [], 18 | create: [], 19 | update: [], 20 | patch: [], 21 | remove: [] 22 | }, 23 | 24 | error: { 25 | all: [], 26 | find: [], 27 | get: [], 28 | create: [], 29 | update: [], 30 | patch: [], 31 | remove: [] 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/services/articles/favorite/favorite.hooks.js: -------------------------------------------------------------------------------- 1 | const { authenticate } = require('@feathersjs/authentication').hooks; 2 | 3 | module.exports = { 4 | before: { 5 | all: [ authenticate('jwt') ], 6 | find: [], 7 | get: [], 8 | create: [], 9 | update: [], 10 | patch: [], 11 | remove: [] 12 | }, 13 | 14 | after: { 15 | all: [], 16 | find: [], 17 | get: [], 18 | create: [], 19 | update: [], 20 | patch: [], 21 | remove: [] 22 | }, 23 | 24 | error: { 25 | all: [], 26 | find: [], 27 | get: [], 28 | create: [], 29 | update: [], 30 | patch: [], 31 | remove: [] 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/services/profiles/profiles.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `profiles` service on path `/profiles` 2 | const createService = require('./profiles.class.js'); 3 | const hooks = require('./profiles.hooks'); 4 | 5 | module.exports = function (app) { 6 | 7 | const paginate = app.get('paginate'); 8 | 9 | const options = { 10 | paginate 11 | }; 12 | 13 | // Initialize our service with any options it requires 14 | app.use('/profiles', createService(options)); 15 | 16 | // Get our initialized service so that we can register hooks 17 | const service = app.service('profiles'); 18 | service.setup(app.service('users')); 19 | 20 | service.hooks(hooks); 21 | }; 22 | -------------------------------------------------------------------------------- /src/hooks/extract-token.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | module.exports = function (options = {}) { 6 | return async context => { 7 | if (context.params && context.params.headers) { 8 | let authorize = context.params.headers.authorization; 9 | 10 | if (authorize) { 11 | let index = authorize.indexOf('Token '); 12 | if (index != -1) { 13 | context.params.headers.authorization = authorize.slice(6); 14 | } 15 | } 16 | } 17 | return context; 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/models/articles.model.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | const mongoose = require('mongoose'); 3 | 4 | module.exports = function (app) { 5 | const Schema = mongoose.Schema; 6 | const ArticlesSchema = new Schema({ 7 | title: String, 8 | description: String, 9 | body: String, 10 | tagList: [String], 11 | userId: Schema.Types.ObjectId, 12 | slug: String, 13 | favoritesCount: { type: Number, default: 0}, 14 | favoritedList: [Schema.Types.ObjectId], 15 | commentId: { type: Number, default: 0} 16 | }, 17 | { 18 | timestamps: true 19 | }); 20 | const Model = mongoose.model('articles', ArticlesSchema); 21 | return Model; 22 | }; 23 | -------------------------------------------------------------------------------- /test/hooks/tag-find.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const tagFind = require('../../src/hooks/tag-find'); 4 | 5 | describe('\'tag-find\' hook', () => { 6 | let app; 7 | 8 | beforeEach(() => { 9 | app = feathers(); 10 | 11 | app.use('/dummy', { 12 | async get(id) { 13 | return { id }; 14 | } 15 | }); 16 | 17 | app.service('dummy').hooks({ 18 | before: tagFind() 19 | }); 20 | }); 21 | 22 | it('runs the hook', async () => { 23 | const result = await app.service('dummy').get('test',{query: {}}); 24 | 25 | assert.deepEqual(result, { id: 'test' }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/hooks/article-get.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | const helpers = require('../common/helpers.js'); 4 | const ferrors = require('@feathersjs/errors'); 5 | 6 | // eslint-disable-next-line no-unused-vars 7 | module.exports = function (options = {}) { 8 | return async context => { 9 | let article = await helpers.getArticles(context,context.id); 10 | if (article.data && article.data.length) { 11 | context.id = article.data[0]._id; 12 | } else { 13 | throw new ferrors.NotFound('Article not found'); 14 | } 15 | 16 | return context; 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/hooks/authenticate.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | module.exports = function (options = {}) { 6 | return async context => { 7 | if (context.data) { 8 | let user2 = context.data.user; 9 | if (user2) { 10 | // RealWorld -- peel off the user wrapper and add strategy if missing 11 | var strategy = user2.strategy; 12 | if (!strategy) { 13 | user2.strategy = 'local'; 14 | } 15 | context.data = user2; 16 | } 17 | } 18 | return context; 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/hooks/process-user.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | module.exports = function (options = {}) { 6 | return async context => { 7 | if (context.data) { 8 | let user = context.data.user; 9 | 10 | if (user) { 11 | user.bio = user.bio ? user.bio : null; 12 | user.image = user.image ? user.image : null; 13 | // RealWorld -- peel off the user wrapper 14 | context.data = context.data.user; 15 | context.data.followingList = []; 16 | } 17 | } 18 | return context; 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /test/hooks/article-find.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const articleFind = require('../../src/hooks/article-find'); 4 | 5 | describe('\'article-find\' hook', () => { 6 | let app; 7 | 8 | beforeEach(() => { 9 | app = feathers(); 10 | 11 | app.use('/dummy', { 12 | async get(id) { 13 | return { id }; 14 | } 15 | }); 16 | 17 | app.service('dummy').hooks({ 18 | before: articleFind() 19 | }); 20 | }); 21 | 22 | it('runs the hook', async () => { 23 | const result = await app.service('dummy').get('test'); 24 | 25 | assert.deepEqual(result, { id: 'test' }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/hooks/authenticate.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const authenticate = require('../../src/hooks/authenticate'); 4 | 5 | describe('\'authenticate\' hook', () => { 6 | let app; 7 | 8 | beforeEach(() => { 9 | app = feathers(); 10 | 11 | app.use('/dummy', { 12 | async get(id) { 13 | return { id }; 14 | } 15 | }); 16 | 17 | app.service('dummy').hooks({ 18 | before: authenticate() 19 | }); 20 | }); 21 | 22 | it('runs the hook', async () => { 23 | const result = await app.service('dummy').get('test'); 24 | 25 | assert.deepEqual(result, { id: 'test' }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/hooks/extract-token.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const extractToken = require('../../src/hooks/extract-token'); 4 | 5 | describe('\'extractToken\' hook', () => { 6 | let app; 7 | 8 | beforeEach(() => { 9 | app = feathers(); 10 | 11 | app.use('/dummy', { 12 | async get(id) { 13 | return { id }; 14 | } 15 | }); 16 | 17 | app.service('dummy').hooks({ 18 | before: extractToken() 19 | }); 20 | }); 21 | 22 | it('runs the hook', async () => { 23 | const result = await app.service('dummy').get('test'); 24 | 25 | assert.deepEqual(result, { id: 'test' }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/hooks/process-user.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const processUser = require('../../src/hooks/process-user'); 4 | 5 | describe('\'process-user\' hook', () => { 6 | let app; 7 | 8 | beforeEach(() => { 9 | app = feathers(); 10 | 11 | app.use('/dummy', { 12 | async get(id) { 13 | return { id }; 14 | } 15 | }); 16 | 17 | app.service('dummy').hooks({ 18 | before: processUser() 19 | }); 20 | }); 21 | 22 | it('runs the hook', async () => { 23 | const result = await app.service('dummy').get('test'); 24 | 25 | assert.deepEqual(result, { id: 'test' }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/hooks/user-response.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const userResponse = require('../../src/hooks/user-response'); 4 | 5 | describe('\'user-response\' hook', () => { 6 | let app; 7 | 8 | beforeEach(() => { 9 | app = feathers(); 10 | 11 | app.use('/dummy', { 12 | async get(id) { 13 | return { id }; 14 | } 15 | }); 16 | 17 | app.service('dummy').hooks({ 18 | after: userResponse() 19 | }); 20 | }); 21 | 22 | it('runs the hook', async () => { 23 | const result = await app.service('dummy').get('test'); 24 | 25 | assert.deepEqual(result, { id: 'test' }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/hooks/users-response.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const usersResponse = require('../../src/hooks/users-response'); 4 | 5 | describe('\'users-response\' hook', () => { 6 | let app; 7 | 8 | beforeEach(() => { 9 | app = feathers(); 10 | 11 | app.use('/dummy', { 12 | async get(id) { 13 | return { id }; 14 | } 15 | }); 16 | 17 | app.service('dummy').hooks({ 18 | after: usersResponse() 19 | }); 20 | }); 21 | 22 | it('runs the hook', async () => { 23 | const result = await app.service('dummy').get('test'); 24 | 25 | assert.deepEqual(result, { id: 'test' }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/hooks/errors-response.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const errorsResponse = require('../../src/hooks/errors-response'); 4 | 5 | describe('\'errors-response\' hook', () => { 6 | let app; 7 | 8 | beforeEach(() => { 9 | app = feathers(); 10 | 11 | app.use('/dummy', { 12 | async get(id) { 13 | return { id }; 14 | } 15 | }); 16 | 17 | app.service('dummy').hooks({ 18 | error: errorsResponse() 19 | }); 20 | }); 21 | 22 | it('runs the hook', async () => { 23 | const result = await app.service('dummy').get('test'); 24 | 25 | assert.deepEqual(result, { id: 'test' }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/hooks/follow-response.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const followResponse = require('../../src/hooks/follow-response'); 4 | 5 | describe('\'follow-response\' hook', () => { 6 | let app; 7 | 8 | beforeEach(() => { 9 | app = feathers(); 10 | 11 | app.use('/dummy', { 12 | async get(id) { 13 | return { id }; 14 | } 15 | }); 16 | 17 | app.service('dummy').hooks({ 18 | after: followResponse() 19 | }); 20 | }); 21 | 22 | it('runs the hook', async () => { 23 | const result = await app.service('dummy').get('test'); 24 | 25 | assert.deepEqual(result, { id: 'test' }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/hooks/tag-find.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | module.exports = function (options = {}) { 6 | return async context => { 7 | if (context.params.query.limit) { 8 | context.params.query.$limit = context.params.query.limit; 9 | delete context.params.query.limit; 10 | } 11 | if (context.params.query.offset) { 12 | context.params.query.$skip = context.params.query.offset; 13 | delete context.params.query.offset; 14 | } 15 | context.params.query.$sort = {popularity: -1}; 16 | return context; 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/services/tags/tags.hooks.js: -------------------------------------------------------------------------------- 1 | 2 | const hideMethod = require('../../hooks/hide-method'); 3 | 4 | const tagFind = require('../../hooks/tag-find'); 5 | 6 | module.exports = { 7 | before: { 8 | all: [], 9 | find: [tagFind()], 10 | get: [], 11 | create: [hideMethod()], 12 | update: [hideMethod()], 13 | patch: [hideMethod()], 14 | remove: [hideMethod()] 15 | }, 16 | 17 | after: { 18 | all: [], 19 | find: [], 20 | get: [], 21 | create: [], 22 | update: [], 23 | patch: [], 24 | remove: [] 25 | }, 26 | 27 | error: { 28 | all: [], 29 | find: [], 30 | get: [], 31 | create: [], 32 | update: [], 33 | patch: [], 34 | remove: [] 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /test/hooks/authenticate-response.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const authenticateResponse = require('../../src/hooks/authenticate-response'); 4 | 5 | describe('\'authenticate-response\' hook', () => { 6 | let app; 7 | 8 | beforeEach(() => { 9 | app = feathers(); 10 | 11 | app.use('/dummy', { 12 | async get(id) { 13 | return { id }; 14 | } 15 | }); 16 | 17 | app.service('dummy').hooks({ 18 | after: authenticateResponse() 19 | }); 20 | }); 21 | 22 | it('runs the hook', async () => { 23 | const result = await app.service('dummy').get('test'); 24 | 25 | assert.deepEqual(result, { id: 'test' }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/models/users.model.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | const mongoose = require('mongoose'); 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | module.exports = function (app) { 6 | const Schema = mongoose.Schema; 7 | const UsersSchema = new Schema({ 8 | username: { type: String, index: true, unique: true, required: [true, 'can\'t be blank'], match: [/^[a-zA-Z0-9_-]+$/, 'is invalid'] }, 9 | email: { type: String, index: true, unique: true, required: [true, 'can\'t be blank'], match: [/\S+@\S+\.\S+/, 'is invalid'] }, 10 | password: String, 11 | bio: String, 12 | image: String, 13 | followingList: [Schema.Types.ObjectId] 14 | }); 15 | const Model = mongoose.model('users', UsersSchema); 16 | return Model; 17 | }; 18 | -------------------------------------------------------------------------------- /src/services/articles/feed/feed.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `articles/feed` service on path `/articles/feed` 2 | const createService = require('./feed.class.js'); 3 | const hooks = require('./feed.hooks'); 4 | const articleResponse = require('../../../middleware/article-response'); 5 | 6 | module.exports = function (app) { 7 | 8 | const paginate = app.get('paginate'); 9 | 10 | const options = { 11 | paginate 12 | }; 13 | 14 | // Initialize our service with any options it requires 15 | app.use('/articles/feed', createService(options),articleResponse); 16 | 17 | // Get our initialized service so that we can register hooks 18 | const service = app.service('articles/feed'); 19 | 20 | service.hooks(hooks); 21 | service.setup(app); 22 | }; 23 | -------------------------------------------------------------------------------- /src/services/profiles/follow/follow.hooks.js: -------------------------------------------------------------------------------- 1 | const { authenticate } = require('@feathersjs/authentication').hooks; 2 | 3 | const followResponse = require('../../../hooks/follow-response'); 4 | 5 | module.exports = { 6 | before: { 7 | all: [ authenticate('jwt') ], 8 | find: [], 9 | get: [], 10 | create: [], 11 | update: [], 12 | patch: [], 13 | remove: [] 14 | }, 15 | 16 | after: { 17 | all: [], 18 | find: [], 19 | get: [], 20 | create: [followResponse()], 21 | update: [], 22 | patch: [], 23 | remove: [followResponse()] 24 | }, 25 | 26 | error: { 27 | all: [], 28 | find: [], 29 | get: [], 30 | create: [], 31 | update: [], 32 | patch: [], 33 | remove: [] 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const logger = require('./logger'); 3 | const app = require('./app'); 4 | const port = app.get('port'); 5 | const express = require('@feathersjs/express'); 6 | 7 | // REalWorld If services prefixed with api/ Then require app.setup(server) 8 | const mainApp = express().use('/api', app); 9 | const server = mainApp.listen(3030); 10 | // Now call setup on the Feathers app with the server 11 | app.setup(server); 12 | // 13 | // Else 14 | //const server = app.listen(port); 15 | // 16 | 17 | process.on('unhandledRejection', (reason, p) => 18 | logger.error('Unhandled Rejection at: Promise ', p, reason) 19 | ); 20 | 21 | server.on('listening', () => 22 | logger.info('Feathers application started on http://%s:%d', app.get('host'), port) 23 | ); 24 | -------------------------------------------------------------------------------- /src/middleware/article-response.js: -------------------------------------------------------------------------------- 1 | 2 | const helpers = require('../common/helpers.js'); 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | module.exports = async function (req,res,next) { 6 | // Insert author details and format result appropriately 7 | 8 | let resultData = await helpers.getAuthorsAndFavorite(res.hook,res.data.data ? res.data.data : [res.data]); 9 | 10 | let result = {}; 11 | 12 | if (!(res.hook.params.query && res.hook.params.query.slug) && (res.hook.method === 'find' || resultData.length > 1)) { 13 | result.articles = resultData; 14 | result.articlesCount = res.hook.result.total ? res.hook.result.total : 0; 15 | } else if (resultData.length > 0) { 16 | result.article = resultData[0]; 17 | } 18 | 19 | res.data = result; 20 | next(); 21 | }; 22 | -------------------------------------------------------------------------------- /src/services/articlecomments/articlecomments.hooks.js: -------------------------------------------------------------------------------- 1 | const { authenticate } = require('@feathersjs/authentication').hooks; 2 | 3 | const hideMethod = require('../../hooks/hide-method'); 4 | 5 | module.exports = { 6 | before: { 7 | all: [hideMethod()], 8 | find: [], 9 | get: [], 10 | create: [authenticate('jwt')], 11 | update: [authenticate('jwt')], 12 | patch: [authenticate('jwt')], 13 | remove: [authenticate('jwt')] 14 | }, 15 | 16 | after: { 17 | all: [], 18 | find: [], 19 | get: [], 20 | create: [], 21 | update: [], 22 | patch: [], 23 | remove: [] 24 | }, 25 | 26 | error: { 27 | all: [], 28 | find: [], 29 | get: [], 30 | create: [], 31 | update: [], 32 | patch: [], 33 | remove: [] 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/app.hooks.js: -------------------------------------------------------------------------------- 1 | // Application hooks that run for every service 2 | const log = require('./hooks/log'); 3 | 4 | const errorsResponse = require('./hooks/errors-response'); 5 | 6 | 7 | const extractToken = require('./hooks/extract-token'); 8 | 9 | 10 | module.exports = { 11 | before: { 12 | all: [log(), extractToken()], 13 | find: [], 14 | get: [], 15 | create: [], 16 | update: [], 17 | patch: [], 18 | remove: [] 19 | }, 20 | 21 | after: { 22 | all: [log()], 23 | find: [], 24 | get: [], 25 | create: [], 26 | update: [], 27 | patch: [], 28 | remove: [] 29 | }, 30 | 31 | error: { 32 | all: [log(), errorsResponse()], 33 | find: [], 34 | get: [], 35 | create: [], 36 | update: [], 37 | patch: [], 38 | remove: [] 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/services/tags/tags.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `tags` service on path `/tags` 2 | const createService = require('feathers-mongoose'); 3 | const createModel = require('../../models/tags.model'); 4 | const hooks = require('./tags.hooks'); 5 | const tagResponse = require('../../middleware/tag-response'); 6 | 7 | 8 | module.exports = function (app) { 9 | const Model = createModel(app); 10 | 11 | const options = { 12 | Model, 13 | lean: true, 14 | paginate: { 15 | default: 20, 16 | max: 50 17 | } 18 | }; 19 | 20 | // Initialize our service with any options it requires 21 | app.use('/tags', createService(options),tagResponse); 22 | 23 | // Get our initialized service so that we can register hooks 24 | const service = app.service('tags'); 25 | 26 | service.hooks(hooks); 27 | }; 28 | -------------------------------------------------------------------------------- /src/hooks/users-response.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | module.exports = function (options = {}) { 6 | return async context => { 7 | 8 | if (context.data) { 9 | context.result.user = { 10 | email: context.data.email, 11 | token: null, 12 | username: context.data.username, 13 | bio: null, 14 | image: null 15 | }; 16 | delete context.result.username; 17 | delete context.result.email; 18 | delete context.result._id; 19 | delete context.result.followingList; 20 | delete context.result.image; 21 | delete context.result.bio; 22 | } 23 | return context; 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/services/user/user.class.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* eslint-disable no-unused-vars */ 4 | class Service { 5 | constructor (options) { 6 | this.options = options || {}; 7 | } 8 | 9 | setup(app) { 10 | this.app = (app); 11 | } 12 | 13 | async find (params) { 14 | let result = {}; 15 | result.email = params.user.email; 16 | result.username = params.user.username; 17 | result.bio = params.user.bio; 18 | result.image = params.user.image; 19 | result.token = params.user.token; 20 | return result; 21 | } 22 | 23 | async update (id, data, params) { 24 | // RealWorld 25 | return this.app.service('users').patch(params.user._id,data.user); 26 | } 27 | } 28 | 29 | module.exports = function (options) { 30 | return new Service(options); 31 | }; 32 | 33 | module.exports.Service = Service; 34 | -------------------------------------------------------------------------------- /src/services/users/users.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `users` service on path `/users` 2 | const createService = require('feathers-mongoose'); 3 | const createModel = require('../../models/users.model'); 4 | const hooks = require('./users.hooks'); 5 | const userCreateLogin = require('../../middleware/user-createlogin'); 6 | 7 | 8 | module.exports = function (app) { 9 | const Model = createModel(app); 10 | const paginate = app.get('paginate'); 11 | 12 | const options = { 13 | Model, 14 | lean: true, 15 | paginate 16 | }; 17 | 18 | // Initialize our service with any options it requires 19 | app.use('/users', createService(options),userCreateLogin); 20 | 21 | // Get our initialized service so that we can register hooks 22 | const service = app.service('users'); 23 | 24 | service.hooks(hooks); 25 | }; 26 | -------------------------------------------------------------------------------- /src/services/articlecomments/articlecomments.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `articlecomments` service on path `/articlecomments` 2 | const createService = require('feathers-mongoose'); 3 | const createModel = require('../../models/articlecomments.model'); 4 | const hooks = require('./articlecomments.hooks'); 5 | 6 | module.exports = function (app) { 7 | const Model = createModel(app); 8 | const paginate = app.get('paginate'); 9 | 10 | const options = { 11 | Model, 12 | lean: true, 13 | paginate, 14 | multi: ['remove'] 15 | }; 16 | 17 | // Initialize our service with any options it requires 18 | app.use('/articlecomments', createService(options)); 19 | 20 | // Get our initialized service so that we can register hooks 21 | const service = app.service('articlecomments'); 22 | 23 | service.hooks(hooks); 24 | }; 25 | -------------------------------------------------------------------------------- /src/services/articles/articles.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `articles` service on path `/articles` 2 | const createService = require('feathers-mongoose'); 3 | const createModel = require('../../models/articles.model'); 4 | const hooks = require('./articles.hooks'); 5 | const articleResponse = require('../../middleware/article-response'); 6 | 7 | module.exports = function (app) { 8 | const Model = createModel(app); 9 | const paginate = app.get('paginate'); 10 | 11 | const options = { 12 | Model, 13 | lean: true, 14 | paginate 15 | }; 16 | 17 | // Initialize our service with any options it requires 18 | app.use('/articles', createService(options),articleResponse); 19 | 20 | // Get our initialized service so that we can register hooks 21 | const service = app.service('articles'); 22 | 23 | service.hooks(hooks); 24 | }; 25 | -------------------------------------------------------------------------------- /test/hooks/article-get.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const articleGet = require('../../src/hooks/article-get'); 4 | 5 | describe('\'article-get\' hook', () => { 6 | let app; 7 | 8 | beforeEach(() => { 9 | app = feathers(); 10 | 11 | app.use('/articles', { 12 | async find(data) { 13 | return {data: [{_id: data.query.slug}]}; 14 | } 15 | 16 | }); 17 | 18 | app.use('/dummy', { 19 | async get(id) { 20 | return { id }; 21 | } 22 | }); 23 | 24 | app.service('dummy').hooks({ 25 | before: articleGet() 26 | }); 27 | }); 28 | 29 | it('runs the hook', async () => { 30 | const result = await app.service('dummy').get('test'); 31 | 32 | assert.deepEqual(result, { id: 'test' }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/hooks/authenticate-response.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | module.exports = function (options = {}) { 6 | return async context => { 7 | 8 | if (context.data) { 9 | let user = { 10 | email: context.data.email, 11 | token: context.result.accessToken, 12 | username: context.result.username ? context.result.username : context.params.user.username, 13 | bio: context.params.user.bio ? context.params.user.bio : null, 14 | image: context.params.user.image ? context.params.user.image : null 15 | }; 16 | 17 | let result = {}; 18 | result.user = user; 19 | context.result = result; 20 | } 21 | 22 | return context; 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/services/profiles/profiles.hooks.js: -------------------------------------------------------------------------------- 1 | const { authenticate } = require('@feathersjs/authentication').hooks; 2 | 3 | const profileResponse = require('../../hooks/profile-response'); 4 | 5 | const authenticateif = require('../../hooks/authenticateif'); 6 | 7 | module.exports = { 8 | before: { 9 | all: [], 10 | find: [], 11 | get: [authenticateif()], 12 | create: [authenticate('jwt')], 13 | update: [authenticate('jwt')], 14 | patch: [authenticate('jwt')], 15 | remove: [authenticate('jwt')] 16 | }, 17 | 18 | after: { 19 | all: [profileResponse()], 20 | find: [], 21 | get: [], 22 | create: [], 23 | update: [], 24 | patch: [], 25 | remove: [] 26 | }, 27 | 28 | error: { 29 | all: [], 30 | find: [], 31 | get: [], 32 | create: [], 33 | update: [], 34 | patch: [], 35 | remove: [] 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/hooks/user-response.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | module.exports = function (options = {}) { 6 | return async context => { 7 | if (context.data) { 8 | 9 | if (context.method === 'update' || context.method === 'find') { 10 | if (context.path === 'user' && !context.result.user) { 11 | let result = {}; 12 | result.user = context.result; 13 | result.user.token = context.params.headers.authorization; 14 | result.user.bio = result.user.bio ? result.user.bio : null; 15 | result.user.image = result.user.image ? result.user.image : null; 16 | delete result.user.password; 17 | context.result = result; 18 | } 19 | } 20 | } 21 | return context; 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /test/hooks/article-delete-response.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const articleDeleteResponse = require('../../src/hooks/article-delete-response'); 4 | 5 | describe('\'article-delete-response\' hook', () => { 6 | let app; 7 | 8 | beforeEach(() => { 9 | app = feathers(); 10 | 11 | app.use('/articlecomments', { 12 | /* eslint-disable no-unused-vars */ 13 | async remove(id,query) { 14 | return { id }; 15 | } 16 | }); 17 | 18 | app.use('/dummy', { 19 | async remove(id) { 20 | return { id }; 21 | } 22 | }); 23 | 24 | app.service('dummy').hooks({ 25 | after: articleDeleteResponse() 26 | }); 27 | }); 28 | 29 | it('runs the hook', async () => { 30 | const result = await app.service('dummy').remove('test'); 31 | 32 | assert.deepEqual(result, { id: 'test' }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/services/articles/comments/comments.hooks.js: -------------------------------------------------------------------------------- 1 | const { authenticate } = require('@feathersjs/authentication').hooks; 2 | 3 | const commentsResponse = require('../../../hooks/comments-response'); 4 | const authenticateif = require('../../../hooks/authenticateif'); 5 | 6 | module.exports = { 7 | before: { 8 | all: [], 9 | find: [authenticateif()], 10 | get: [authenticateif()], 11 | create: [authenticate('jwt')], 12 | update: [authenticate('jwt')], 13 | patch: [authenticate('jwt')], 14 | remove: [authenticate('jwt')] 15 | }, 16 | 17 | after: { 18 | all: [], 19 | find: [commentsResponse()], 20 | get: [commentsResponse()], 21 | create: [commentsResponse()], 22 | update: [], 23 | patch: [], 24 | remove: [] 25 | }, 26 | 27 | error: { 28 | all: [], 29 | find: [], 30 | get: [], 31 | create: [], 32 | update: [], 33 | patch: [], 34 | remove: [] 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/hooks/log.js: -------------------------------------------------------------------------------- 1 | // A hook that logs service method before, after and error 2 | // See https://github.com/winstonjs/winston for documentation 3 | // about the logger. 4 | const logger = require('../logger'); 5 | const util = require('util'); 6 | 7 | // To see more detailed messages, uncomment the following line: 8 | // logger.level = 'debug'; 9 | 10 | module.exports = function () { 11 | return context => { 12 | // This debugs the service call and a stringified version of the hook context 13 | // You can customize the message (and logger) to your needs 14 | logger.debug(`${context.type} app.service('${context.path}').${context.method}()`); 15 | 16 | if(typeof context.toJSON === 'function' && logger.level === 'debug') { 17 | logger.debug('Hook Context', util.inspect(context, {colors: false})); 18 | } 19 | 20 | if(context.error && !context.result) { 21 | logger.error(context.error.stack); 22 | } 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/services/user/user.hooks.js: -------------------------------------------------------------------------------- 1 | const { authenticate } = require('@feathersjs/authentication').hooks; 2 | const userResponse = require('../../hooks/user-response'); 3 | 4 | const { 5 | protect 6 | } = require('@feathersjs/authentication-local').hooks; 7 | 8 | module.exports = { 9 | before: { 10 | all: [ authenticate('jwt') ], 11 | find: [], 12 | get: [], 13 | create: [], 14 | update: [], 15 | patch: [], 16 | remove: [] 17 | }, 18 | 19 | after: { 20 | all: [userResponse(), 21 | // Make sure the password field is never sent to the client 22 | // Always must be the last hook 23 | protect('password') 24 | ], 25 | find: [], 26 | get: [], 27 | create: [], 28 | update: [], 29 | patch: [], 30 | remove: [] 31 | }, 32 | 33 | error: { 34 | all: [], 35 | find: [], 36 | get: [], 37 | create: [], 38 | update: [], 39 | patch: [], 40 | remove: [] 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /test/hooks/article-delete.test.js: -------------------------------------------------------------------------------- 1 | //const assert = require('assert'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const articleDelete = require('../../src/hooks/article-delete'); 4 | 5 | describe('\'article-delete\' hook', () => { 6 | let app; 7 | 8 | beforeEach(() => { 9 | app = feathers(); 10 | 11 | app.use('/articles', { 12 | async find(data) { 13 | return {data: [{userId: 'ab1',data:data}]}; 14 | }, 15 | async get(data) { 16 | return {data}; 17 | } 18 | }); 19 | app.use('/dummy', { 20 | async remove(id) { 21 | return { id }; 22 | }, 23 | async create(data) { 24 | return { data }; 25 | } 26 | }); 27 | 28 | app.service('dummy').hooks({ 29 | before: articleDelete() 30 | }); 31 | }); 32 | 33 | it('runs the hook', async () => { 34 | 35 | await app.service('dummy').remove('foo',{user: {_id: 'ab1'}}); 36 | 37 | //assert.deepEqual(result, { id: 'test' }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/hooks/follow-response.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | const helpers = require('../common/helpers.js'); 4 | 5 | // eslint-disable-next-line no-unused-vars 6 | module.exports = function (options = {}) { 7 | return async context => { 8 | if (context.result) { 9 | let result = {}; 10 | let author = context.result; 11 | 12 | if (author && author.data && author.data.length) { 13 | result.profile = { 14 | username: author.data[0].username, 15 | bio: author.data[0].bio ? author.data[0].bio : null, 16 | image: author.data[0].image ? author.data[0].image : null, 17 | following: helpers.findIndex(context.params.user.followingList,author.data[0]._id) != -1 ? true : false 18 | }; 19 | context.result = result; 20 | if (context.method == 'create') { 21 | context.statusCode = '200'; 22 | } 23 | } 24 | } 25 | return context; 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/services/index.js: -------------------------------------------------------------------------------- 1 | const users = require('./users/users.service.js'); 2 | const articles = require('./articles/articles.service.js'); 3 | const user = require('./user/user.service.js'); 4 | const profiles = require('./profiles/profiles.service.js'); 5 | const follow = require('./profiles/follow/follow.service.js'); 6 | const favorite = require('./articles/favorite/favorite.service.js'); 7 | const articlecomments = require('./articlecomments/articlecomments.service.js'); 8 | const comments = require('./articles/comments/comments.service.js'); 9 | const tags = require('./tags/tags.service.js'); 10 | const articlesFeed = require('./articles/feed/feed.service.js'); 11 | // eslint-disable-next-line no-unused-vars 12 | module.exports = function (app) { 13 | app.configure(users); 14 | app.configure(articlesFeed); 15 | app.configure(articles); 16 | app.configure(user); 17 | app.configure(profiles); 18 | app.configure(follow); 19 | app.configure(favorite); 20 | app.configure(articlecomments); 21 | app.configure(comments); 22 | app.configure(tags); 23 | }; 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Feathers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/authentication.js: -------------------------------------------------------------------------------- 1 | const authentication = require('@feathersjs/authentication'); 2 | const jwt = require('@feathersjs/authentication-jwt'); 3 | const local = require('@feathersjs/authentication-local'); 4 | const authenticate = require('./hooks/authenticate'); 5 | const authenticateResponse = require('./hooks/authenticate-response'); 6 | 7 | 8 | module.exports = function (app) { 9 | const config = app.get('authentication'); 10 | // Set up authentication with the secret 11 | app.configure(authentication(config)); 12 | app.configure(jwt()); 13 | app.configure(local()); 14 | app.service('users/login'); 15 | // The `authentication` service is used to create a JWT. 16 | // The before `create` hook registers strategies that can be used 17 | // to create a new valid JWT (e.g. local or oauth2) 18 | // RealWorld use users/login 19 | app.service('users/login').hooks({ 20 | before: { 21 | create: [ 22 | authenticate(), 23 | authentication.hooks.authenticate(config.strategies), 24 | 25 | ], 26 | remove: [ 27 | authentication.hooks.authenticate('jwt') 28 | ] 29 | }, 30 | after: { 31 | create: [ 32 | authenticateResponse() 33 | ] 34 | } 35 | }); 36 | 37 | }; 38 | -------------------------------------------------------------------------------- /src/services/users/users.hooks.js: -------------------------------------------------------------------------------- 1 | const { authenticate } = require('@feathersjs/authentication').hooks; 2 | 3 | const { 4 | hashPassword, protect 5 | } = require('@feathersjs/authentication-local').hooks; 6 | 7 | const processUser = require('../../hooks/process-user'); 8 | const usersResponse = require('../../hooks/users-response'); 9 | const hideMethod = require('../../hooks/hide-method'); 10 | 11 | module.exports = { 12 | before: { 13 | all: [processUser()], 14 | find: [ authenticate('jwt') ], 15 | get: [ authenticate('jwt') ], 16 | create: [ hashPassword() ], 17 | update: [ hashPassword(), authenticate('jwt') ], 18 | patch: [ hashPassword(), authenticate('jwt') ], 19 | remove: [ authenticate('jwt'),hideMethod() ] 20 | }, 21 | 22 | after: { 23 | all: [ 24 | // Make sure the password field is never sent to the client 25 | // Always must be the last hook 26 | protect('password') 27 | ], 28 | find: [], 29 | get: [], 30 | create: [usersResponse()], 31 | update: [], 32 | patch: [], 33 | remove: [] 34 | }, 35 | 36 | error: { 37 | all: [], 38 | find: [], 39 | get: [], 40 | create: [], 41 | update: [], 42 | patch: [], 43 | remove: [] 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/services/articles/feed/feed.class.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | const ferrors = require('@feathersjs/errors'); 4 | 5 | class Service { 6 | constructor (options) { 7 | this.options = options || {}; 8 | } 9 | 10 | setup(app) { 11 | this.app = (app); 12 | } 13 | 14 | async find (params) { 15 | let articles = {}; 16 | 17 | if (params.user.followingList && params.user.followingList.length > 0) { 18 | articles = await this.getFeed(params,params.user.followingList); 19 | } 20 | return articles; 21 | } 22 | 23 | async getFeed(params,following) { 24 | let thequery = {query: {$sort: {createdAt: -1}}}; 25 | if (params.query.limit) { 26 | thequery.query.$limit = params.query.limit; 27 | } 28 | if (params.query.offset) { 29 | thequery.query.$skip = params.query.offset; 30 | } 31 | thequery.query.userId = {$in: following}; 32 | let article = this.app.service('articles').find(thequery); 33 | 34 | article.catch(function () { 35 | throw new ferrors.NotFound('Articles not found'); 36 | }); 37 | 38 | return article; 39 | } 40 | 41 | } 42 | 43 | module.exports = function (options) { 44 | return new Service(options); 45 | }; 46 | 47 | module.exports.Service = Service; 48 | -------------------------------------------------------------------------------- /src/services/articles/comments/comments.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `comments` service on path `/comments` 2 | const createService = require('./comments.class.js'); 3 | const hooks = require('./comments.hooks'); 4 | 5 | module.exports = function (app) { 6 | 7 | const paginate = app.get('paginate'); 8 | 9 | const options = { 10 | paginate 11 | }; 12 | 13 | // Initialize our service with any options it requires 14 | app.use('/articles/:slug/comments', createService(options)); 15 | 16 | // A hook that updates `data` with the route parameter 17 | function mapSlugToData(context) { 18 | if(context.data && context.params.route.slug) { 19 | context.data.slug = context.params.route.slug; 20 | } 21 | } 22 | 23 | // For the new route, map the :slug route parameter to the query in a hook 24 | app.service('articles/:slug/comments').hooks({ 25 | before: { 26 | find(context) { 27 | context.params.query.slug = context.params.route.slug; 28 | }, 29 | create: mapSlugToData, 30 | remove: mapSlugToData 31 | } 32 | }); 33 | 34 | // Get our initialized service so that we can register hooks 35 | const service = app.service('articles/:slug/comments'); 36 | service.setup(app); 37 | 38 | service.hooks(hooks); 39 | }; 40 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "localhost", 3 | "port": 3030, 4 | "public": "../public/", 5 | "paginate": { 6 | "default": 10, 7 | "max": 50 8 | }, 9 | "authentication": { 10 | "secret": "2f976e4551984a082c03c139b96fba7981a2505557ccebace51b98b457195f79c013904a3a093378c6d11b94da32bf4705024d74b20429aa138843fca7413093806c016b14f2f4c33e59c89cf1ad7da62dc5bae22d41b5e43db0f0c95fc683a64eb8017536aa1c45e54545a82932d307f8ef591b3a85047708aec7d020a972c328b654799dc5ec88d28f4fa72dffbb8719c79888f5ed1e8f0641c55bf1104112a7348531f59fa2f68259337e75c0fa0850cc340c3d8ba07baadb80e1a9e41fee4fe7f7ae7695d007f89bb04a25f91bf72fb1eeddc1100b8e9bb6a553402a652c586daf7330579b6246b73951460c488e3278f414602446af70e4b2671b5e927d", 11 | "strategies": [ 12 | "jwt", 13 | "local" 14 | ], 15 | "path": "/users/login", 16 | "service": "users", 17 | "jwt": { 18 | "header": { 19 | "typ": "access" 20 | }, 21 | "audience": "https://yourdomain.com", 22 | "subject": "anonymous", 23 | "issuer": "feathers", 24 | "algorithm": "HS256", 25 | "expiresIn": "1d" 26 | }, 27 | "local": { 28 | "entity": "user", 29 | "usernameField": "email", 30 | "passwordField": "password" 31 | } 32 | }, 33 | "nedb": "../data" 34 | } 35 | -------------------------------------------------------------------------------- /src/services/profiles/follow/follow.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `follow` service on path `/follow` 2 | const createService = require('./follow.class.js'); 3 | const hooks = require('./follow.hooks'); 4 | 5 | module.exports = function (app) { 6 | 7 | const paginate = app.get('paginate'); 8 | 9 | const options = { 10 | paginate 11 | }; 12 | 13 | // Initialize our service with any options it requires 14 | app.use('/profiles/:username/follow', createService(options)); 15 | 16 | // A hook that updates `data` with the route parameter 17 | function mapUserNameToData(context) { 18 | if(context.data && context.params.route.username) { 19 | context.data.username = context.params.route.username; 20 | } 21 | } 22 | 23 | // For the new route, map the `:username` route parameter to the query in a hook 24 | app.service('profiles/:username/follow').hooks({ 25 | before: { 26 | find(context) { 27 | context.params.query.username = context.params.route.username; 28 | }, 29 | create: mapUserNameToData, 30 | remove: mapUserNameToData 31 | } 32 | }); 33 | 34 | // Get our initialized service so that we can register hooks 35 | const service = app.service('profiles/:username/follow'); 36 | service.setup(app); 37 | 38 | service.hooks(hooks); 39 | }; 40 | -------------------------------------------------------------------------------- /src/services/articles/articles.hooks.js: -------------------------------------------------------------------------------- 1 | 2 | const { authenticate } = require('@feathersjs/authentication').hooks; 3 | 4 | const articleCreate = require('../../hooks/article-create'); 5 | 6 | const articleFind = require('../../hooks/article-find'); 7 | 8 | const articleGet = require('../../hooks/article-get'); 9 | 10 | const articleUpdate = require('../../hooks/article-update'); 11 | 12 | const articleDelete = require('../../hooks/article-delete'); 13 | 14 | const authenticateif = require('../../hooks/authenticateif'); 15 | 16 | const articleDeleteResponse = require('../../hooks/article-delete-response'); 17 | 18 | module.exports = { 19 | before: { 20 | all: [], 21 | find: [authenticateif(),articleFind()], 22 | get: [authenticateif(),articleGet()], 23 | create: [authenticate('jwt'), articleCreate()], 24 | update: [authenticate('jwt'), articleUpdate()], 25 | patch: [authenticate('jwt')], 26 | remove: [authenticate('jwt'), articleDelete()] 27 | }, 28 | 29 | after: { 30 | all: [], 31 | find: [], 32 | get: [], 33 | create: [], 34 | update: [], 35 | patch: [], 36 | remove: [articleDeleteResponse()] 37 | }, 38 | 39 | error: { 40 | all: [], 41 | find: [], 42 | get: [], 43 | create: [], 44 | update: [], 45 | patch: [], 46 | remove: [] 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/hooks/article-create.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | const helpers = require('../common/helpers.js'); 4 | 5 | // eslint-disable-next-line no-unused-vars 6 | module.exports = function (options = {}) { 7 | return async context => { 8 | context.data.article.userId = context.params.user._id; 9 | context.data.article.slug = helpers.getSlug(context.data.article.title); 10 | context.data.article.favoritesCount = 0; 11 | context.data.article.favorited = false; 12 | context.data.article.favoritedList = []; 13 | context.data.article.commentid = 0; 14 | 15 | if (context.data.article.tagList) { 16 | context.data.article.tagList.forEach(async function(tag) { 17 | let tagret = await context.app.service('tags').find({query: {name: tag}}); 18 | if (tagret && tagret.data && tagret.data.length) { 19 | tagret.data[0].popularity = tagret.data[0].popularity +1; 20 | await context.app.service('tags').update(tagret.data[0]._id,tagret.data[0]); 21 | } else { 22 | await context.app.service('tags').create({name: tag, popularity: 1}); 23 | } 24 | }); 25 | } 26 | context.data = context.data.article; 27 | 28 | return context; 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/hooks/article-update.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | const helpers = require('../common/helpers.js'); 4 | const ferrors = require('@feathersjs/errors'); 5 | 6 | // eslint-disable-next-line no-unused-vars 7 | module.exports = function (options = {}) { 8 | return async context => { 9 | 10 | let article = await helpers.getArticles(context,context.id); 11 | 12 | if (!article.data || ! article.data.length){ 13 | throw new ferrors.NotFound('Article not found'); 14 | } 15 | if (article.data[0].userId.toString() != context.params.user._id){ 16 | throw new ferrors.Forbidden(); 17 | } 18 | 19 | if (context.data.article.title && context.data.article.title != article.data[0].title) { 20 | context.data.article.slug = helpers.getSlug(context.data.article.title); 21 | } 22 | 23 | article = await patchArticle(context,article.data[0]._id,context.data.article); 24 | context.result = {data: [article]}; 25 | 26 | return context; 27 | }; 28 | }; 29 | 30 | function patchArticle(context,id,articlenew) { 31 | let article = context.app.service('articles').patch(id,articlenew); 32 | article.catch(function () { 33 | throw new ferrors.NotFound('Article not updated'); 34 | }); 35 | 36 | return article; 37 | } 38 | -------------------------------------------------------------------------------- /src/services/articles/favorite/favorite.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `favorite` service on path `/favorite` 2 | const createService = require('./favorite.class.js'); 3 | const hooks = require('./favorite.hooks'); 4 | const articleResponse = require('../../../middleware/article-response'); 5 | 6 | module.exports = function (app) { 7 | 8 | const paginate = app.get('paginate'); 9 | 10 | const options = { 11 | paginate 12 | }; 13 | 14 | // Initialize our service with any options it requires 15 | app.use('/articles/:slug/favorite',createService(options),articleResponse); 16 | 17 | // A hook that updates `data` with the route parameter 18 | function mapSlugToData(context) { 19 | if(context.data && context.params.route.slug) { 20 | context.data.slug = context.params.route.slug; 21 | } 22 | } 23 | 24 | // For the new route, map the `:slug` route parameter to the query in a hook 25 | app.service('articles/:slug/favorite').hooks({ 26 | before: { 27 | find(context) { 28 | context.params.query.slug = context.params.route.slug; 29 | }, 30 | create: mapSlugToData, 31 | remove: mapSlugToData 32 | } 33 | }); 34 | 35 | // Get our initialized service so that we can register hooks 36 | const service = app.service('articles/:slug/favorite'); 37 | service.setup(app); 38 | 39 | service.hooks(hooks); 40 | }; 41 | -------------------------------------------------------------------------------- /test/hooks/article-update.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const articleUpdate = require('../../src/hooks/article-update'); 4 | 5 | describe('\'article-update\' hook', () => { 6 | let app; 7 | 8 | beforeEach(() => { 9 | app = feathers(); 10 | 11 | app.use('/articles', { 12 | async find(data) { 13 | return {data: [{_id: 1,userId: 'ab1',title: data}]}; 14 | }, 15 | async patch(id,data) { 16 | return data; 17 | } 18 | }); 19 | 20 | app.use('/dummy', { 21 | async update(id,data) { 22 | return { data }; 23 | } 24 | }); 25 | 26 | app.service('dummy').hooks({ 27 | before: articleUpdate() 28 | }); 29 | }); 30 | 31 | it('runs the hook', async () => { 32 | const result = await app.service('dummy').update(1,{ 33 | article: { 34 | title: 'How to train your dragon', 35 | description: 'Ever wonder how?', 36 | body: 'You have to believe' 37 | } 38 | }, 39 | {user: {_id: 'ab1'}}); 40 | assert.deepEqual(result.data[0].body,'You have to believe'); 41 | assert.deepEqual(result.data[0].description, 'Ever wonder how?'); 42 | let slug = 'How-to-train-your-dragon_'; 43 | assert.deepEqual(result.data[0].slug.slice(0,slug.length),slug); 44 | assert.deepEqual(result.data[0].title, 'How to train your dragon'); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/hooks/profile-response.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | const helpers = require('../common/helpers.js'); 4 | const ferrors = require('@feathersjs/errors'); 5 | 6 | // eslint-disable-next-line no-unused-vars 7 | module.exports = function (options = {}) { 8 | return async context => { 9 | if (context.result) { 10 | let result = {}; 11 | 12 | if (context.result.data.length > 0) { 13 | let userss = context.result.data[0]; 14 | let following = false; 15 | if (context.params.user) { 16 | let author = await helpers.getAuthorByName(context,context.params.user.username); 17 | if (author.data && author.data.length) { 18 | if (helpers.findIndex(author.data[0].followingList,userss._id) != -1) { 19 | following = true; 20 | } 21 | } 22 | } 23 | //let following = Array.isArray(userss.following) ? true > 0 : false; 24 | result.profile = { 25 | username: userss.username, 26 | bio: userss.bio ? userss.bio : null, 27 | image: userss.image ? userss.image : null, 28 | following: following 29 | }; 30 | context.result = result; 31 | } else { 32 | throw new ferrors.NotFound('User not found'); 33 | } 34 | } 35 | 36 | return context; 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /src/hooks/article-delete.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | const helpers = require('../common/helpers.js'); 4 | const ferrors = require('@feathersjs/errors'); 5 | 6 | // eslint-disable-next-line no-unused-vars 7 | module.exports = function (options = {}) { 8 | return async context => { 9 | 10 | let article = await helpers.getArticles(context,context.id); 11 | 12 | if (article && article.data && article.data.length) { 13 | if (article.data[0].userId.toString() != context.params.user._id){ 14 | throw new ferrors.Forbidden(); 15 | } 16 | context.id = article.data[0]._id; 17 | if (article.data[0].tagList) { 18 | article.data[0].tagList.forEach(async function(tag) { 19 | let tagret = await context.app.service('tags').find({query: {name: tag}}); 20 | if (tagret && tagret.data && tagret.data.length) { 21 | if (tagret.data[0].popularity <= 1) { 22 | await context.app.service('tags').remove(tagret.data[0]._id); 23 | } else { 24 | tagret.data[0].popularity = tagret.data[0].popularity -1; 25 | await context.app.service('tags').update(tagret.data[0]._id,tagret.data[0]); 26 | } 27 | } 28 | }); 29 | } 30 | } else { 31 | throw new ferrors.NotFound('Article not found'); 32 | } 33 | 34 | return context; 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /test/app.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const rp = require('request-promise'); 3 | const url = require('url'); 4 | const app = require('../src/app'); 5 | 6 | const port = app.get('port') || 3030; 7 | const getUrl = pathname => url.format({ 8 | hostname: app.get('host') || 'localhost', 9 | protocol: 'http', 10 | port, 11 | pathname 12 | }); 13 | 14 | describe('Feathers application tests', () => { 15 | before(function(done) { 16 | this.server = app.listen(port); 17 | this.server.once('listening', () => done()); 18 | }); 19 | 20 | after(function(done) { 21 | this.server.close(done); 22 | }); 23 | 24 | it('starts and shows the index page', () => { 25 | return rp(getUrl()).then(body => 26 | assert.ok(body.indexOf('') !== -1) 27 | ); 28 | }); 29 | 30 | describe('404', function() { 31 | it('shows a 404 HTML page', () => { 32 | return rp({ 33 | url: getUrl('path/to/nowhere'), 34 | headers: { 35 | 'Accept': 'text/html' 36 | } 37 | }).catch(res => { 38 | assert.equal(res.statusCode, 404); 39 | assert.ok(res.error.indexOf('') !== -1); 40 | }); 41 | }); 42 | 43 | it('shows a 404 JSON error without stack trace', () => { 44 | return rp({ 45 | url: getUrl('path/to/nowhere'), 46 | json: true 47 | }).catch(res => { 48 | assert.equal(res.statusCode, 404); 49 | assert.equal(res.error.code, 404); 50 | assert.equal(res.error.message, 'Page not found'); 51 | assert.equal(res.error.name, 'NotFound'); 52 | }); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/hooks/article-find.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | const helpers = require('../common/helpers.js'); 4 | 5 | // eslint-disable-next-line no-unused-vars 6 | module.exports = function (options = {}) { 7 | return async context => { 8 | 9 | if (!context.params.query) { 10 | context.params.query = {}; 11 | } 12 | context.params.query.$sort = {createdAt: -1}; 13 | 14 | if (context.params.query.limit) { 15 | context.params.query.$limit = context.params.query.limit; 16 | delete context.params.query.limit; 17 | } 18 | if (context.params.query.offset) { 19 | context.params.query.$skip = context.params.query.offset; 20 | delete context.params.query.offset; 21 | } 22 | if (context.params.query.tag) { 23 | context.params.query.tagList = context.params.query.tag; 24 | delete context.params.query.tag; 25 | } 26 | if (context.params.query.author) { 27 | let author = await helpers.getAuthorByName(context,context.params.query.author); 28 | if (author.data && author.data.length) { 29 | context.params.query.userId = author.data[0]._id; 30 | delete context.params.query.author; 31 | } 32 | } 33 | if (context.params.query.favorited) { 34 | let user1 = await helpers.getAuthorByName(context,context.params.query.favorited); 35 | if (user1.data && user1.data.length > 0) { 36 | delete context.params.query.favorited; 37 | context.params.query.favoritedList = user1.data[0]._id; 38 | } 39 | } 40 | 41 | return context; 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /test/hooks/comments-response.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const commentsResponse = require('../../src/hooks/comments-response'); 4 | 5 | describe('\'comments-response\' hook', () => { 6 | let app; 7 | 8 | beforeEach(() => { 9 | app = feathers(); 10 | 11 | app.use('/users', { 12 | // eslint-disable-next-line no-unused-vars 13 | async find(id) { 14 | return {data: [{username: 'foo', bio: 'bar',image: 'foobar', _id: 1},{username: 'foo', bio: 'bar',image: 'foobar', _id: 2}]}; 15 | } 16 | }); 17 | 18 | app.use('/dummy', { 19 | async find(id) { 20 | return id; 21 | } 22 | }); 23 | 24 | app.service('dummy').hooks({ 25 | after: commentsResponse() 26 | }); 27 | }); 28 | 29 | it('runs the hook', async () => { 30 | const result = await app.service('dummy').find({data: 31 | [ 32 | { 33 | body: 'Really Awesome!', 34 | id: 2, 35 | userId: 1 36 | }, 37 | { 38 | body: 'Really Awesome!', 39 | id: 3, 40 | userId: 2 41 | } 42 | ] 43 | }); 44 | 45 | assert.deepEqual(result, {comments: [ 46 | { 47 | author: { 48 | bio: 'bar', 49 | following: false, 50 | image: 'foobar', 51 | username: 'foo' 52 | }, 53 | body: 'Really Awesome!', 54 | id: 2 55 | }, 56 | { 57 | author: { 58 | bio: 'bar', 59 | following: false, 60 | image: 'foobar', 61 | username: 'foo' 62 | }, 63 | body: 'Really Awesome!', 64 | id: 3 65 | } 66 | ]}); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/services/profiles.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('../../src/app'); 3 | 4 | describe('\'profiles\' service', () => { 5 | let user; 6 | 7 | it('registered the service', () => { 8 | const service = app.service('user'); 9 | 10 | assert.ok(service, 'Registered the service'); 11 | }); 12 | 13 | it('creates a user, encrypts password', async () => { 14 | // Setting `provider` indicates an external request 15 | const params = { provider: 'rest' }; 16 | 17 | user = await app.service('users').create({user: { 18 | username: 'test', 19 | email: 'test@example.com', 20 | password: 'secret' 21 | }}, params); 22 | 23 | // Makes sure the password got encrypted 24 | assert.ok(user.password !== 'secret'); 25 | }); 26 | 27 | it('logs in correctly', async () => { 28 | // Setting `provider` indicates an external request 29 | const params = { provider: 'rest' }; 30 | 31 | user = await app.service('users/login').create({user: { 32 | email: 'test@example.com', 33 | password: 'secret' 34 | }}, params); 35 | 36 | // Make sure password has been removed 37 | assert.ok(!user.password); 38 | }); 39 | 40 | it('gets profile', async () => { 41 | // Setting `provider` indicates an external request 42 | const params = { provider: 'rest' }; 43 | 44 | user = await app.service('profiles').get(user.user.username, params); 45 | 46 | // Make sure password has been removed 47 | assert.ok(user.profile.following === false); 48 | }); 49 | 50 | it('cleans up', async () => { 51 | user = await app.service('users').find({query: {username: user.profile.username}}); 52 | 53 | await app.service('users').remove(user.data[0]._id); 54 | 55 | }); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /test/hooks/profile-response.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const memory = require('feathers-memory'); 4 | const profileResponse = require('../../src/hooks/profile-response'); 5 | 6 | describe('\'profile-response\' hook', () => { 7 | let app, user, user2; 8 | 9 | beforeEach(async () => { 10 | app = feathers(); 11 | 12 | // Database adapter pagination options 13 | const options = { 14 | paginate: { 15 | default: 10, 16 | max: 25 17 | } 18 | }; 19 | 20 | // Register `users` and `messages` service in-memory 21 | app.use('/users', memory(options)); 22 | 23 | app.use('/profiles', { 24 | // eslint-disable-next-line no-unused-vars 25 | async get(id) { 26 | return {data: [{username: 'bar', bio: 'b',image: 'foobar', _id: 'bar'}]}; 27 | } 28 | }); 29 | 30 | app.service('profiles').hooks({ 31 | after: profileResponse() 32 | }); 33 | 34 | // Create a new user we can use to test with 35 | user = await app.service('users').create({username: 'foo', bio: 'b',image: 'foobar', id: 'foo', followingList: ['bar']}); 36 | user2 = await app.service('users').create({username: 'foo2', bio: 'b',image: 'foobar', id: 'foo2', followingList: []}); 37 | 38 | }); 39 | 40 | it('runs the hook', async () => { 41 | const result = await app.service('profiles').get('bar',{user: {username: user.username}}); 42 | 43 | assert.deepEqual(result, {profile: {username: 'bar', bio: 'b',image: 'foobar', following: true}}); 44 | 45 | const result2 = await app.service('profiles').get('bar',{user: {username: user2.username}}); 46 | 47 | assert.deepEqual(result2, {profile: {username: 'bar', bio: 'b',image: 'foobar', following: false}}); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/services/user.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('../../src/app'); 3 | 4 | describe('\'user\' service', () => { 5 | let user; 6 | 7 | it('registered the service', () => { 8 | const service = app.service('user'); 9 | 10 | assert.ok(service, 'Registered the service'); 11 | }); 12 | 13 | it('creates a user, encrypts password', async () => { 14 | // Setting `provider` indicates an external request 15 | const params = { provider: 'rest' }; 16 | 17 | user = await app.service('users').create({user: { 18 | username: 'test', 19 | email: 'test@example.com', 20 | password: 'secret' 21 | }}, params); 22 | 23 | // Makes sure the password got encrypted 24 | assert.ok(user.password !== 'secret'); 25 | }); 26 | 27 | it('logs in correctly', async () => { 28 | // Setting `provider` indicates an external request 29 | const params = { provider: 'rest' }; 30 | 31 | user = await app.service('users/login').create({user: { 32 | email: 'test@example.com', 33 | password: 'secret' 34 | }}, params); 35 | 36 | // Make sure password has been removed 37 | assert.ok(!user.password); 38 | }); 39 | 40 | it('gets current user', async () => { 41 | // Setting `provider` indicates an external request 42 | const params = { provider: 'rest' }; 43 | //token = user.user.token; 44 | user = await app.service('user').find(user, params); 45 | 46 | // Make sure password has been removed 47 | assert.ok(!user.password); 48 | assert.ok(user.email === 'test@example.com'); 49 | assert.ok(user.username === 'test'); 50 | }); 51 | 52 | it('cleans up', async () => { 53 | 54 | user = await app.service('users').find({query: {username: user.username}}); 55 | 56 | await app.service('users').remove(user.data[0]._id); 57 | 58 | }); 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "real-world-feathers", 3 | "description": "Real World implementation for Feathers", 4 | "version": "0.0.0", 5 | "homepage": "", 6 | "main": "src", 7 | "keywords": [ 8 | "feathers" 9 | ], 10 | "author": { 11 | "name": "Randy Smithey", 12 | "email": "randy@blindspotcreative.com" 13 | }, 14 | "contributors": [], 15 | "bugs": {}, 16 | "directories": { 17 | "lib": "src", 18 | "test": "test/", 19 | "config": "config/" 20 | }, 21 | "engines": { 22 | "node": "^10.0.0", 23 | "npm": ">= 3.0.0" 24 | }, 25 | "scripts": { 26 | "test": "npm run eslint && NODE_ENV= npm run mocha", 27 | "eslint": "eslint src/. test/. --config .eslintrc.json", 28 | "dev": "nodemon src/", 29 | "start": "node src/", 30 | "mocha": "mocha test/ --recursive --exit" 31 | }, 32 | "dependencies": { 33 | "@feathersjs/authentication": "^2.1.15", 34 | "@feathersjs/authentication-jwt": "^2.0.9", 35 | "@feathersjs/authentication-local": "^1.2.9", 36 | "@feathersjs/configuration": "^2.0.6", 37 | "@feathersjs/errors": "^3.3.6", 38 | "@feathersjs/express": "^1.3.0", 39 | "@feathersjs/feathers": "^3.3.0", 40 | "compression": "^1.7.3", 41 | "cors": "^2.8.5", 42 | "feathers-mongoose": "^7.3.0", 43 | "feathers-nedb": "^4.0.0", 44 | "helmet": "^3.15.0", 45 | "mongoose": "^5.4.8", 46 | "nedb": "^1.8.0", 47 | "serve-favicon": "^2.5.0", 48 | "slug": "^0.9.3", 49 | "winston": "^3.1.0" 50 | }, 51 | "devDependencies": { 52 | "@feathersjs/authentication-client": "^1.0.11", 53 | "@feathersjs/client": "^3.7.8", 54 | "@feathersjs/rest-client": "^1.4.7", 55 | "eslint": "^5.11.1", 56 | "feathers-memory": "^3.0.2", 57 | "mocha": "^5.2.0", 58 | "node-fetch": "^2.3.0", 59 | "nodemon": "^1.18.9", 60 | "request": "^2.88.0", 61 | "request-promise": "^4.2.2" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/services/profiles/follow/follow.class.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | const helpers = require('../../../common/helpers.js'); 3 | const ferrors = require('@feathersjs/errors'); 4 | 5 | class Service { 6 | constructor (options) { 7 | this.options = options || {}; 8 | } 9 | 10 | setup(app) { 11 | this.app = (app); 12 | } 13 | 14 | async create (data, params) { 15 | let user1 = await helpers.getUserByName(this,data.username); 16 | if (!user1.data || !user1.data.length) { 17 | throw new ferrors.NotFound('User not found'); 18 | } 19 | let user2 = {}; 20 | user2.followingList = [user1.data[0]._id]; 21 | if (params.user.followingList) { 22 | if (helpers.findIndex(params.user.followingList,user1.data[0]._id) == -1) { 23 | user2.followingList = params.user.followingList.concat(user2.followingList); 24 | } else { 25 | user2.followingList = params.user.followingList; 26 | } 27 | } 28 | await this.app.service('users').patch(params.user._id,user2); 29 | params.user.followingList = user2.followingList; 30 | return user1; 31 | } 32 | 33 | async remove (id, params) { 34 | let user1 = await helpers.getUserByName(this,params.route.username); 35 | if (!user1.data || !user1.data.length) { 36 | throw new ferrors.NotFound('User not found'); 37 | } 38 | let userList = params.user.followingList; 39 | let index = helpers.findIndex(userList,user1.data[0]._id); 40 | if (index != -1){ 41 | userList.splice(index,1); 42 | } 43 | let user2 = {}; 44 | user2.followingList = userList; 45 | await this.app.service('users').patch(params.user._id,user2); 46 | params.user.followingList = user2.followingList; 47 | return user1; 48 | } 49 | 50 | } 51 | 52 | module.exports = function (options) { 53 | return new Service(options); 54 | }; 55 | 56 | module.exports.Service = Service; 57 | -------------------------------------------------------------------------------- /test/services/users.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('../../src/app'); 3 | 4 | describe('\'users\' service', () => { 5 | let user,user2; 6 | 7 | it('registered the service', () => { 8 | const service = app.service('users'); 9 | 10 | assert.ok(service, 'Registered the service'); 11 | }); 12 | 13 | it('creates a user, encrypts password', async () => { 14 | user = await app.service('users').create({user: { 15 | username: 'test', 16 | email: 'test@example.com', 17 | password: 'secret' 18 | }}); 19 | 20 | // Makes sure the password got encrypted 21 | assert.ok(user.password !== 'secret'); 22 | }); 23 | 24 | it('removes password for external requests', async () => { 25 | // Setting `provider` indicates an external request 26 | const params = { provider: 'rest' }; 27 | 28 | user2 = await app.service('users').create({user: { 29 | username: 'test2', 30 | email: 'test2@example.com', 31 | password: 'secret' 32 | }}, params); 33 | 34 | // Make sure password has been removed 35 | assert.ok(!user2.password); 36 | }); 37 | it('logs in correctly', async () => { 38 | // Setting `provider` indicates an external request 39 | const params = { provider: 'rest' }; 40 | 41 | user2 = await app.service('users/login').create({user: { 42 | email: 'test2@example.com', 43 | password: 'secret' 44 | }}, params); 45 | 46 | // Make sure password has been removed 47 | assert.ok(!user2.password); 48 | }); 49 | 50 | it('cleans up', async () => { 51 | 52 | user = await app.service('users').find({query: {username: user.user.username}}); 53 | user2 = await app.service('users').find({query: {username: user2.user.username}}); 54 | 55 | await app.service('users').remove(user.data[0]._id); 56 | await app.service('users').remove(user2.data[0]._id); 57 | 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/hooks/comments-response.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | 4 | const helpers = require('../common/helpers.js'); 5 | 6 | // eslint-disable-next-line no-unused-vars 7 | module.exports = function (options = {}) { 8 | return async context => { 9 | let result = {}; 10 | 11 | if (context.result.errors) { 12 | return context; 13 | } 14 | 15 | let resultData = await getAuthors(context); 16 | 17 | if (context.method == 'find' || resultData.length > 1) { 18 | result.comments = resultData; 19 | } else if (resultData.length > 0) { 20 | result.comment = resultData[0]; 21 | } else { 22 | result.comments = []; 23 | } 24 | 25 | context.result = result; 26 | return context; 27 | }; 28 | }; 29 | 30 | async function getAuthors(context) { 31 | let resultData = []; 32 | let theResult = context.result; 33 | 34 | if (!theResult.data) { 35 | theResult= {}; 36 | theResult.data = [context.result]; 37 | } 38 | 39 | let authorids = []; 40 | theResult.data.forEach(function(element) { 41 | authorids.push(element.userId); 42 | }); 43 | 44 | let authors = await helpers.getAuthors(context,authorids); 45 | 46 | theResult.data.forEach(function(element) { 47 | let comment = element; 48 | let theauthor = authors.data.find(function(item) { 49 | return item._id.toString() == this.authorid; 50 | },{authorid: comment.userId}); 51 | 52 | if (theauthor) { 53 | comment.author = {username: theauthor.username, bio: theauthor.bio, image: theauthor.image, following: false}; 54 | if (context.params.user) { 55 | comment.author.following = helpers.findIndex(context.params.user.followingList,comment.userId) != -1 ? true : false; 56 | } 57 | resultData.push(comment); 58 | } 59 | delete comment.userId; 60 | delete comment.articleId; 61 | delete comment._id; 62 | 63 | }); 64 | 65 | return resultData; 66 | } 67 | -------------------------------------------------------------------------------- /test/hooks/article-create.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const memory = require('feathers-memory'); 4 | const articleCreate = require('../../src/hooks/article-create'); 5 | 6 | describe('\'article-create\' hook', () => { 7 | let app; 8 | 9 | beforeEach(() => { 10 | app = feathers(); 11 | 12 | // Database adapter pagination options 13 | const options = { 14 | paginate: { 15 | default: 10, 16 | max: 25 17 | } 18 | }; 19 | 20 | // articleCreate hook uses tags service so we need a stub here 21 | app.use('/tags', { 22 | async create(data) { 23 | return {data}; 24 | }, 25 | async find(data) { 26 | return {data}; 27 | } 28 | }); 29 | 30 | // Register `users` and `messages` service in-memory 31 | app.use('/articles', memory(options)); 32 | 33 | app.service('articles').hooks({ 34 | before: articleCreate(app) 35 | }); 36 | }); 37 | 38 | it('runs the hook', async () => { 39 | const result = await app.service('articles').create({ 40 | article: { 41 | title: 'How to train your dragon', 42 | description: 'Ever wonder how?', 43 | body: 'You have to believe', 44 | tagList: ['reactjs', 'angularjs', 'dragons'] 45 | } 46 | }, 47 | { 48 | user: {_id: 1} 49 | }); 50 | 51 | assert.deepEqual(result.body,'You have to believe'); 52 | assert.deepEqual(result.commentid, 0); 53 | assert.deepEqual(result.description, 'Ever wonder how?'); 54 | assert.deepEqual(result.favorited, false); 55 | assert.deepEqual(result.favoritedList, []); 56 | assert.deepEqual(result.favoritesCount, 0); 57 | let slug = 'How-to-train-your-dragon_'; 58 | assert.deepEqual(result.slug.slice(0,slug.length),slug); 59 | assert.deepEqual(result.tagList, ['reactjs','angularjs','dragons']); 60 | assert.deepEqual(result.title, 'How to train your dragon'); 61 | assert.deepEqual(result.userId, 1); 62 | 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const favicon = require('serve-favicon'); 3 | const compress = require('compression'); 4 | const helmet = require('helmet'); 5 | const cors = require('cors'); 6 | const logger = require('./logger'); 7 | 8 | const feathers = require('@feathersjs/feathers'); 9 | const configuration = require('@feathersjs/configuration'); 10 | const express = require('@feathersjs/express'); 11 | 12 | const mongoose = require('mongoose'); 13 | mongoose.Promise = global.Promise; 14 | // Connect to your MongoDB instance(s) using env variable 15 | logger.info(process.env.MONGODB_FEATHERS_REALWORLD); 16 | mongoose.connect(process.env.MONGODB_FEATHERS_REALWORLD,{ useNewUrlParser: true, useFindAndModify: false, useCreateIndex: true }); 17 | 18 | const middleware = require('./middleware'); 19 | const services = require('./services'); 20 | const appHooks = require('./app.hooks'); 21 | const channels = require('./channels'); 22 | 23 | const authentication = require('./authentication'); 24 | 25 | const app = express(feathers()); 26 | 27 | // Load app configuration 28 | app.configure(configuration()); 29 | // Enable security, CORS, compression, favicon and body parsing 30 | app.use(helmet()); 31 | app.use(cors()); 32 | app.use(compress()); 33 | app.use(express.json()); 34 | app.use(express.urlencoded({ extended: true })); 35 | app.use(favicon(path.join(app.get('public'), 'favicon.ico'))); 36 | // Host the public folder 37 | app.use('/', express.static(app.get('public'))); 38 | 39 | // Set up Plugins and providers 40 | app.configure(express.rest()); 41 | 42 | // Configure other middleware (see `middleware/index.js`) 43 | app.configure(middleware); 44 | app.configure(authentication); 45 | // Set up our services (see `services/index.js`) 46 | app.configure(services); 47 | // Set up event channels (see channels.js) 48 | app.configure(channels); 49 | 50 | // Configure a middleware for 404s and the error handler 51 | app.use(express.notFound()); 52 | app.use(express.errorHandler({ logger })); 53 | 54 | app.hooks(appHooks); 55 | 56 | module.exports = app; 57 | -------------------------------------------------------------------------------- /test/services/articles.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('../../src/app'); 3 | 4 | // This test calls services directly 5 | 6 | describe('\'articles\' service', () => { 7 | let user, article; 8 | 9 | it('registered the service', () => { 10 | const service = app.service('user'); 11 | 12 | assert.ok(service, 'Registered the service'); 13 | }); 14 | 15 | it('creates a user, encrypts password', async () => { 16 | // Setting `provider` indicates an external request 17 | const params = { provider: 'rest' }; 18 | 19 | user = await app.service('users').create({user: { 20 | username: 'test', 21 | email: 'test@example.com', 22 | password: 'secret' 23 | }}, params); 24 | 25 | // Makes sure the password got encrypted 26 | assert.ok(user.password !== 'secret'); 27 | }); 28 | 29 | it('logs in correctly', async () => { 30 | // Setting `provider` indicates an external request 31 | const params = { provider: 'rest' }; 32 | 33 | user = await app.service('users/login').create({user: { 34 | email: 'test@example.com', 35 | password: 'secret' 36 | }}, params); 37 | 38 | // Make sure password has been removed 39 | assert.ok(!user.password); 40 | }); 41 | 42 | it('Creates an Article', async () => { 43 | // Setting `provider` indicates an external request 44 | const params = {provider: 'rest', headers: {authorization: user.user.token}}; 45 | article = await app.service('articles').create({article: {title: 'a title', description: 'adescription', body: 'abody', tagList: ['one','two','three']}}, params); 46 | 47 | let slug = 'a-title_'; 48 | assert.deepEqual(article.slug.slice(0,slug.length),slug); 49 | }); 50 | 51 | it('cleans up', async () => { 52 | 53 | let user2 = await app.service('users').find({query: {username: user.user.username}}); 54 | 55 | const params = {provider: 'rest', headers: {authorization: user.user.token}}; 56 | await app.service('articles').remove(article.slug, params); 57 | await app.service('users').remove(user2.data[0]._id); 58 | 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | #  2 | 3 | [](http://realworld.io) 4 | [](https://travis-ci.com/randyscotsmithey/feathers-realworld-example-app) 5 | 6 | 7 | > ### Feathers codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld) spec and API. 8 | 9 | 10 | ### [Demo](https://github.com/gothinkster/realworld) [RealWorld](https://github.com/gothinkster/realworld) 11 | 12 | 13 | This codebase was created to demonstrate a fully fledged fullstack application built with Feathers (and Mongoose) including CRUD operations, authentication, routing, pagination, and more. 14 | 15 | We've gone to great lengths to adhere to the Feathers community styleguides & best practices. 16 | 17 | For more information on how to this works with other frontends/backends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repo. 18 | 19 | 20 | # How it works 21 | 22 | [Feathers](https://feathersjs.com/) is a service oriented REST and Real Time API layer. Endpoints are defined as services. 23 | 24 | ## Application Structure 25 | 26 | - `app.js` - Entry point for the application. 27 | - `config/` - Configuration/environment variables for the server. 28 | - `src/services/` - Service implementations 29 | - `src/middleware/` - Middleware implementations 30 | - `src/models/` - Schema definitions for our Mongoose models(Easily changed to use other databases). 31 | - `src/hooks/` - Hooks for running code before and after services. Used for formatting requests and responses correctly. 32 | - `src/common/` - Common helper functions. 33 | 34 | 35 | # Getting started 36 | 37 | To get the Feathers server running locally: 38 | 39 | - Clone this repo 40 | - `cd feathers-realworld-example-app` 41 | - `npm install` to install all required dependencies 42 | - Set environment variable MONGODB_FEATHERS_REALWORLD to the connection string for your mongodb instance (You can install a community edition [instructions](https://docs.mongodb.com/manual/installation/#tutorials) or create an instance on mLab: [instructions](https://docs.mlab.com/)). 43 | - `npm start` to start the local server (or `npm test` to run tests). 44 | -------------------------------------------------------------------------------- /src/services/articles/favorite/favorite.class.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | const helpers = require('../../../common/helpers.js'); 4 | const ferrors = require('@feathersjs/errors'); 5 | 6 | class Service { 7 | constructor (options) { 8 | this.options = options || {}; 9 | } 10 | 11 | setup(app) { 12 | this.app = (app); 13 | } 14 | 15 | async create (data, params) { 16 | let user1 = await helpers.getUserByName(this,params.user.username); 17 | let articleUpdate = {}; 18 | articleUpdate.favoritedList = [user1.data[0]._id]; 19 | let article = await helpers.getArticles(this,data.slug); 20 | 21 | if (article && article.data && article.data.length) { 22 | if (article.data[0].favoritedList) { 23 | if (helpers.findIndex(article.data[0].favoritedList,user1.data[0]._id) == -1) { 24 | articleUpdate.favoritedList = article.data[0].favoritedList.concat(articleUpdate.favoritedList); 25 | } else { 26 | articleUpdate.favoritedList = article.data[0].favoritedList; 27 | } 28 | } 29 | articleUpdate.favorited = true; 30 | articleUpdate.favoritesCount = articleUpdate.favoritedList.length; 31 | 32 | return await this.app.service('articles').patch(article.data[0]._id,articleUpdate); 33 | } 34 | throw new ferrors.NotFound('Article not found'); 35 | } 36 | 37 | async remove (id, params) { 38 | let article = await helpers.getArticles(this,params.route.slug); 39 | 40 | if (article && article.data && article.data.length) { 41 | if (article.data[0].favoritedList) { 42 | let favoriteList = article.data[0].favoritedList; 43 | let user1 = await helpers.getUserByName(this,params.user.username); 44 | let index = helpers.findIndex(favoriteList,user1.data[0]._id); 45 | if (index != -1){ 46 | favoriteList.splice(index,1); 47 | } 48 | let articleUpdate = {}; 49 | articleUpdate.favorited = favoriteList.length > 0 ? true : false; 50 | articleUpdate.favoritedList = favoriteList; 51 | articleUpdate.favoritesCount = articleUpdate.favoritedList.length; 52 | return await this.app.service('articles').patch(article.data[0]._id,articleUpdate); 53 | } 54 | } 55 | 56 | throw new ferrors.NotFound('Article not found'); 57 | } 58 | } 59 | 60 | module.exports = function (options) { 61 | return new Service(options); 62 | }; 63 | 64 | module.exports.Service = Service; 65 | -------------------------------------------------------------------------------- /src/channels.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | if(typeof app.channel !== 'function') { 3 | // If no real-time functionality has been configured just return 4 | return; 5 | } 6 | 7 | app.on('connection', connection => { 8 | // On a new real-time connection, add it to the anonymous channel 9 | app.channel('anonymous').join(connection); 10 | }); 11 | 12 | app.on('login', (authResult, { connection }) => { 13 | // connection can be undefined if there is no 14 | // real-time connection, e.g. when logging in via REST 15 | if(connection) { 16 | // Obtain the logged in user from the connection 17 | // const user = connection.user; 18 | 19 | // The connection is no longer anonymous, remove it 20 | app.channel('anonymous').leave(connection); 21 | 22 | // Add it to the authenticated user channel 23 | app.channel('authenticated').join(connection); 24 | 25 | // Channels can be named anything and joined on any condition 26 | 27 | // E.g. to send real-time events only to admins use 28 | // if(user.isAdmin) { app.channel('admins').join(connection); } 29 | 30 | // If the user has joined e.g. chat rooms 31 | // if(Array.isArray(user.rooms)) user.rooms.forEach(room => app.channel(`rooms/${room.id}`).join(channel)); 32 | 33 | // Easily organize users by email and userid for things like messaging 34 | // app.channel(`emails/${user.email}`).join(channel); 35 | // app.channel(`userIds/$(user.id}`).join(channel); 36 | } 37 | }); 38 | 39 | // eslint-disable-next-line no-unused-vars 40 | app.publish((data, hook) => { 41 | // Here you can add event publishers to channels set up in `channels.js` 42 | // To publish only for a specific event use `app.publish(eventname, () => {})` 43 | 44 | console.log('Publishing all events to all authenticated users. See `channels.js` and https://docs.feathersjs.com/api/channels.html for more information.'); // eslint-disable-line 45 | 46 | // e.g. to publish all service events to all authenticated users use 47 | return app.channel('authenticated'); 48 | }); 49 | 50 | // Here you can also add service specific event publishers 51 | // e.g. the publish the `users` service `created` event to the `admins` channel 52 | // app.service('users').publish('created', () => app.channel('admins')); 53 | 54 | // With the userid and email organization from above you can easily select involved users 55 | // app.service('messages').publish(() => { 56 | // return [ 57 | // app.channel(`userIds/${data.createdBy}`), 58 | // app.channel(`emails/${data.recipientEmail}`) 59 | // ]; 60 | // }); 61 | }; 62 | -------------------------------------------------------------------------------- /src/services/articles/comments/comments.class.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | const helpers = require('../../../common/helpers.js'); 4 | const ferrors = require('@feathersjs/errors'); 5 | 6 | class Service { 7 | constructor (options) { 8 | this.options = options || {}; 9 | } 10 | 11 | setup(app) { 12 | this.app = (app); 13 | } 14 | 15 | async find (params) { 16 | 17 | let article = await helpers.getArticles(this,params.query.slug); 18 | 19 | if (article && article.data && article.data.length) { 20 | return this.app.service('articlecomments').find({ 21 | query: { 22 | articleId: article.data[0]._id, 23 | $sort: {id: 1} 24 | } 25 | }); 26 | } 27 | throw new ferrors.NotFound('Article: ' + params.query.slug + ' not found'); 28 | } 29 | 30 | async get (id, params) { 31 | 32 | let article = await helpers.getArticles(this,params.query.slug); 33 | 34 | if (article && article.data && article.data.length) { 35 | return this.app.service('articlecomments').find({ 36 | query: { 37 | articleId: article.data[0]._id, 38 | id: id 39 | } 40 | }); 41 | } 42 | throw new ferrors.NotFound('Article: ' + params.query.slug + ' not found'); 43 | } 44 | 45 | async create (data, params) { 46 | 47 | let article = await helpers.getArticles(this,data.slug); 48 | 49 | if (article && article.data && article.data.length) { 50 | let comment = { body: data.comment.body}; 51 | comment.articleId = article.data[0]._id; 52 | comment.userId = params.user._id; 53 | comment.id = article.data[0].commentId ? article.data[0].commentId + 1 : 1; 54 | 55 | let articlepatched = await this.app.service('articles').patch(article.data[0]._id,{commentId: comment.id}); 56 | return this.app.service('articlecomments').create(comment); 57 | } 58 | throw new ferrors.NotFound('Article: ' + data.slug + ' not found'); 59 | } 60 | 61 | async remove (id, params) { 62 | let thequery = { 63 | query: { 64 | slug: params.route.slug 65 | } 66 | }; 67 | let article = await this.app.service('articles').find(thequery); 68 | 69 | if (article && article.data && article.data.length) { 70 | let comment = await this.app.service('articlecomments').find({ 71 | query: { 72 | articleId: article.data[0]._id, 73 | id: parseInt(id) 74 | } 75 | }); 76 | 77 | if (comment.data && comment.data.length > 0) { 78 | if (comment.data[0].userId.toString() != params.user._id){ 79 | throw new ferrors.Forbidden(); 80 | } 81 | return this.app.service('articlecomments').remove(comment.data[0]._id); 82 | } 83 | } 84 | 85 | throw new ferrors.NotFound('Comment not found'); 86 | } 87 | } 88 | 89 | module.exports = function (options) { 90 | return new Service(options); 91 | }; 92 | 93 | module.exports.Service = Service; 94 | -------------------------------------------------------------------------------- /src/common/helpers.js: -------------------------------------------------------------------------------- 1 | const slug = require('slug'); 2 | const ferrors = require('@feathersjs/errors'); 3 | 4 | function getSlug(title) { 5 | return slug(title) + '_' + getAnId(); 6 | } 7 | 8 | function getAnId() { 9 | return Math.random().toString(36).substr(2, 9); 10 | } 11 | 12 | function getAuthor(context,userId) { 13 | let author = context.app.service('users').find({ 14 | query: { 15 | _id: userId 16 | } 17 | }); 18 | author.catch(function () { 19 | throw new ferrors.NotFound('Author not found'); 20 | }); 21 | 22 | return author; 23 | } 24 | 25 | function getAuthorByName(context,username) { 26 | return getUserByName(context,username); 27 | } 28 | 29 | function getUserByName(context,username) { 30 | let author = context.app.service('users').find({ 31 | query: { 32 | username: username 33 | } 34 | }); 35 | author.catch(function () { 36 | throw new ferrors.NotFound('User not found'); 37 | }); 38 | 39 | return author; 40 | } 41 | 42 | function getAuthors(context,authorids) { 43 | 44 | let authors = context.app.service('users').find({ 45 | query: { 46 | _id: { 47 | $in: authorids 48 | } 49 | } 50 | }); 51 | authors.catch(function () { 52 | throw new ferrors.NotFound('Author not found'); 53 | }); 54 | 55 | return authors; 56 | } 57 | 58 | async function getAuthorsAndFavorite(context,thelist) { 59 | let resultdata = []; 60 | 61 | let authorids = []; 62 | thelist.forEach(function(element) { 63 | authorids.push(element.userId); 64 | }); 65 | 66 | let authors = await getAuthors(context,authorids); 67 | 68 | thelist.forEach(function(element) { 69 | let article = element; 70 | 71 | let theauthor = authors.data.find(function(item) { 72 | return item._id.toString() == this.authorid; 73 | },{authorid: article.userId}); 74 | 75 | if (theauthor) { 76 | article.author = {username: theauthor.username, bio: theauthor.bio ? theauthor.bio : null, image: theauthor.image ? theauthor.image : null, following: false}; 77 | article.favorited = false; 78 | if (context.params.user) { 79 | article.author.following = findIndex(context.params.user.followingList,article.userId) != -1 ? true : false; 80 | article.favorited = article.favoritedList && findIndex(article.favoritedList,context.params.user._id) != -1 ? true : false; 81 | } 82 | delete article.favoritedList; 83 | delete article.commentId; 84 | resultdata.push(article); 85 | } 86 | }); 87 | 88 | return resultdata; 89 | } 90 | 91 | function getArticles(context,theslug) { 92 | 93 | let article = context.app.service('articles').find({ 94 | query: { 95 | slug: theslug 96 | } 97 | }); 98 | article.catch(function () { 99 | throw new ferrors.NotFound('Article not found'); 100 | }); 101 | 102 | return article; 103 | } 104 | 105 | 106 | function findIndex(theList,theElement) { 107 | return theList.findIndex(function(element) {return element.toString() == this.tofind;},{tofind: theElement}); 108 | } 109 | 110 | module.exports.getAuthor = getAuthor; 111 | module.exports.getAuthors = getAuthors; 112 | module.exports.getAuthorsAndFavorite = getAuthorsAndFavorite; 113 | module.exports.getAuthorByName = getAuthorByName; 114 | module.exports.getUserByName = getUserByName; 115 | module.exports.getArticles = getArticles; 116 | module.exports.getSlug = getSlug; 117 | module.exports.getAnId = getAnId; 118 | module.exports.findIndex = findIndex; 119 | -------------------------------------------------------------------------------- /test/services/tags.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const feathersClient = require('@feathersjs/rest-client'); 4 | const auth = require('@feathersjs/authentication-client'); 5 | const fetch = require('node-fetch'); 6 | const app = require('../../src/app'); 7 | 8 | const host = app.get('host'); 9 | const port = app.get('port'); 10 | const username = 'testclient'; 11 | const email = 'testclient@example.com'; 12 | const password = 'secret'; 13 | 14 | describe('\'tags\' service', () => { 15 | it('registered the service', () => { 16 | const service = app.service('tags'); 17 | 18 | assert.ok(service, 'Registered the service'); 19 | }); 20 | 21 | let server; 22 | let client; 23 | 24 | let user,article,article2,article3,article4; 25 | 26 | before(async () => { 27 | 28 | server = app.listen(port); 29 | server.on('listening', async () => { 30 | // eslint-disable-next-line no-console 31 | console.log('Feathers application started on http://%s:%d', host, port); 32 | }); 33 | 34 | client = await makeClient(); 35 | }); 36 | 37 | after(() => { 38 | client.logout(); 39 | server.close(); 40 | }); 41 | 42 | describe('Run tests using client and server', () => { 43 | 44 | it('registered the service', () => { 45 | const service = client.service('users'); 46 | assert.ok(service, 'Registered the client service'); 47 | }); 48 | 49 | it('creates a user, encrypts password, logs in', async () => { 50 | user = await client.service('users').create({user: { 51 | username: username, 52 | email: email, 53 | password: password 54 | }}); 55 | 56 | // Makes sure the password got encrypted 57 | assert.ok(user.password !== 'secret'); 58 | 59 | }); 60 | 61 | it('Creates an Article with tags', async () => { 62 | article = await client.service('articles').create({article: {title: 'a title', description: 'adescription', body: 'abody', tagList: ['tagone','tagtwo']}},{headers: {authorization: user.user.token}}); 63 | 64 | assert.deepEqual(article.article.tagList, ['tagone','tagtwo']); 65 | 66 | let tags = await client.service('tags').find(); 67 | assert.ok(tags.tags.length === 2); 68 | assert.ok(tags.tags[0] === 'tagone' || tags.tags[0] === 'tagtwo'); 69 | assert.ok(tags.tags[1] === 'tagone' || tags.tags[1] === 'tagtwo'); 70 | }); 71 | 72 | it('Adds another article checks for the tag order', async () => { 73 | 74 | article2 = await client.service('articles').create({article: {title: 'another title', description: 'adescription', body: 'abody', tagList: ['tagthree','tagtwo']}},{headers: {authorization: user.user.token}}); 75 | 76 | assert.deepEqual(article2.article.tagList, ['tagthree','tagtwo']); 77 | 78 | let tags = await client.service('tags').find(); 79 | assert.ok(tags.tags.length === 3); 80 | assert.ok(tags.tags[0] === 'tagtwo'); 81 | assert.ok(tags.tags[1] === 'tagone' || tags.tags[1] === 'tagthree'); 82 | assert.ok(tags.tags[2] === 'tagone' || tags.tags[2] === 'tagthree'); 83 | }); 84 | 85 | it('Adds a third article checks for the tag order', async () => { 86 | 87 | article3 = await client.service('articles').create({article: {title: 'a third title', description: 'adescription', body: 'abody', tagList: ['tagthree','tagtwo']}},{headers: {authorization: user.user.token}}); 88 | 89 | assert.deepEqual(article3.article.tagList, ['tagthree','tagtwo']); 90 | 91 | let tags = await client.service('tags').find(); 92 | assert.ok(tags.tags.length === 3); 93 | assert.ok(tags.tags[0] === 'tagtwo'); 94 | assert.ok(tags.tags[1] === 'tagthree'); 95 | assert.ok(tags.tags[2] === 'tagone'); 96 | }); 97 | 98 | it('Adds a fourth article checks for the tag order', async () => { 99 | 100 | article4 = await client.service('articles').create({article: {title: 'a fourth title', description: 'adescription', body: 'abody', tagList: ['tagtwo']}},{headers: {authorization: user.user.token}}); 101 | 102 | assert.deepEqual(article4.article.tagList, ['tagtwo']); 103 | 104 | let tags = await client.service('tags').find(); 105 | assert.ok(tags.tags.length === 3); 106 | assert.ok(tags.tags[0] === 'tagtwo'); 107 | assert.ok(tags.tags[1] === 'tagthree'); 108 | assert.ok(tags.tags[2] === 'tagone'); 109 | }); 110 | 111 | it('cleans up first article and checks tags', async () => { 112 | 113 | await client.service('articles').remove(article.article.slug,{headers: {authorization: user.user.token}}); 114 | 115 | let tags = await client.service('tags').find(); 116 | assert.ok(tags.tags.length === 2); 117 | assert.ok(tags.tags[0] === 'tagtwo'); 118 | assert.ok(tags.tags[1] === 'tagthree'); 119 | 120 | }); 121 | 122 | it('cleans up another article and checks tags', async () => { 123 | 124 | await client.service('articles').remove(article2.article.slug,{headers: {authorization: user.user.token}}); 125 | 126 | let tags = await client.service('tags').find(); 127 | assert.ok(tags.tags.length === 2); 128 | assert.ok(tags.tags[0] === 'tagtwo'); 129 | assert.ok(tags.tags[1] === 'tagthree'); 130 | 131 | }); 132 | 133 | it('cleans up third article and checks tags', async () => { 134 | 135 | await client.service('articles').remove(article3.article.slug,{headers: {authorization: user.user.token}}); 136 | 137 | let tags = await client.service('tags').find(); 138 | assert.ok(tags.tags.length === 1); 139 | assert.ok(tags.tags[0] === 'tagtwo'); 140 | 141 | }); 142 | 143 | 144 | /*function fillTests() { 145 | let thetests = []; 146 | for (let i = 0; i < 450; i++) { 147 | thetests.push({title: 'title' + i.toString(), description: 'description', body: 'body', tagList: ['taga' + i.toString(), 'tagb' + i.toString(),'atag']}); 148 | } 149 | return thetests; 150 | } 151 | 152 | let tests = fillTests(); 153 | let articles = []; 154 | 155 | tests.forEach(function(test) { 156 | it('In Loop Creates an Article with tags' , async () => { 157 | article = await client.service('articles').create({article: {title: test.title, description: test.description, body: test.body, tagList: test.tagList}},{headers: {authorization: user.user.token}}); 158 | articles.push(article); 159 | assert.deepEqual(article.article.tagList, test.tagList); 160 | 161 | }); 162 | }); 163 | 164 | tests.forEach(function(test,index) { 165 | it('In Loop cleans up first article and checks tags', async () => { 166 | 167 | await client.service('articles').remove(articles[index].article.slug,{headers: {authorization: user.user.token}}); 168 | 169 | assert.deepEqual(articles[index].article.tagList, test.tagList); 170 | }); 171 | });*/ 172 | 173 | it('cleans up fourth article and checks tags and cleans up user', async () => { 174 | 175 | await client.service('articles').remove(article4.article.slug,{headers: {authorization: user.user.token}}); 176 | 177 | let tags = await client.service('tags').find(); 178 | assert.ok(tags.tags.length === 0); 179 | 180 | let user2 = await client.service('users').find({query: {username: user.user.username},headers: {authorization: user.user.token}}); 181 | await app.service('users').remove(user2.data[0]._id); 182 | 183 | }); 184 | }); 185 | }); 186 | 187 | async function makeClient() { 188 | var client = feathers(); 189 | // Configure the REST client to use 'node-fetch' 190 | const rest = feathersClient('http://localhost:3030'); 191 | client.configure(rest.fetch(fetch)); 192 | client.configure(auth({ 193 | path: 'users/login', 194 | storage: localStorage() 195 | })); 196 | 197 | return client; 198 | } 199 | 200 | function localStorage () { 201 | const store = {}; 202 | 203 | return { 204 | setItem (key, value) { 205 | store[key] = value; 206 | }, 207 | getItem (key) { 208 | return store[key]; 209 | }, 210 | removeItem (key) { 211 | delete store[key]; 212 | } 213 | }; 214 | } 215 | -------------------------------------------------------------------------------- /test/services/client-articles.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const feathersClient = require('@feathersjs/rest-client'); 4 | const auth = require('@feathersjs/authentication-client'); 5 | const fetch = require('node-fetch'); 6 | const app = require('../../src/app'); 7 | 8 | const host = app.get('host'); 9 | const port = app.get('port'); 10 | const email = 'testclient@example.com'; 11 | const password = 'secret'; 12 | 13 | // This test uses a feathers rest client for full round trip testing. 14 | 15 | describe('\'articles\' service - client', () => { 16 | 17 | it('registered the service', () => { 18 | const service = app.service('user'); 19 | 20 | assert.ok(service, 'Registered the service'); 21 | }); 22 | 23 | //this.timeout(10000); 24 | let server; 25 | let client; 26 | 27 | let user,article; 28 | 29 | before(async () => { 30 | 31 | server = app.listen(port); 32 | server.on('listening', async () => { 33 | // eslint-disable-next-line no-console 34 | console.log('Feathers application started on http://%s:%d', host, port); 35 | }); 36 | 37 | client = await makeClient(); 38 | }); 39 | 40 | after(() => { 41 | client.logout(); 42 | server.close(); 43 | }); 44 | 45 | describe('Run tests using client and server', () => { 46 | 47 | it('registered the service', () => { 48 | 49 | const service = client.service('users'); 50 | assert.ok(service, 'Registered the client service'); 51 | }); 52 | 53 | it('creates a user, encrypts password, logs in', async () => { 54 | user = await client.service('users').create({user: { 55 | username: 'testclient', 56 | email: 'testclient@example.com', 57 | password: 'secret' 58 | }}); 59 | 60 | // Makes sure the password got encrypted 61 | assert.ok(user.password !== 'secret'); 62 | 63 | }); 64 | 65 | it('Logs in', async () => { 66 | 67 | /*user = await client.service('users/login').create({user: { 68 | email: email, 69 | password: password 70 | }});*/ 71 | 72 | user = await client.authenticate({ 73 | strategy: 'local', 74 | email, 75 | password 76 | }); 77 | 78 | }); 79 | 80 | it('Creates an Article', async () => { 81 | 82 | article = await client.service('articles').create({article: {title: 'a title', description: 'adescription', body: 'abody', tagList: ['one','two','three']}}); 83 | 84 | let slug = 'a-title_'; 85 | assert.deepEqual(article.article.slug.slice(0,slug.length),slug); 86 | assert.deepEqual(article.article.body,'abody'); 87 | assert.deepEqual(article.article.description, 'adescription'); 88 | assert.deepEqual(article.article.favorited, false); 89 | assert.deepEqual(article.article.favoritesCount, 0); 90 | assert.deepEqual(article.article.tagList, ['one','two','three']); 91 | assert.deepEqual(article.article.title, 'a title'); 92 | assert.deepEqual(article.article.author.username, 'testclient'); 93 | assert.deepEqual(article.article.author.following, false); 94 | 95 | article = await client.service('articles').get(article.article.slug); 96 | 97 | assert.deepEqual(article.article.slug.slice(0,slug.length),slug); 98 | assert.deepEqual(article.article.body,'abody'); 99 | assert.deepEqual(article.article.description, 'adescription'); 100 | assert.deepEqual(article.article.favorited, false); 101 | assert.deepEqual(article.article.favoritesCount, 0); 102 | assert.deepEqual(article.article.tagList, ['one','two','three']); 103 | assert.deepEqual(article.article.title, 'a title'); 104 | assert.deepEqual(article.article.author.username, 'testclient'); 105 | assert.deepEqual(article.article.author.following, false); 106 | 107 | }); 108 | 109 | it('Updates the article and checks for the change', async () => { 110 | await client.service('articles').update(article.article.slug,{article: {description: 'better description'}}); 111 | 112 | article = await client.service('articles').get(article.article.slug); 113 | 114 | let slug = 'a-title_'; 115 | assert.deepEqual(article.article.slug.slice(0,slug.length),slug); 116 | assert.deepEqual(article.article.body,'abody'); 117 | assert.deepEqual(article.article.description, 'better description'); 118 | assert.deepEqual(article.article.favorited, false); 119 | assert.deepEqual(article.article.favoritesCount, 0); 120 | assert.deepEqual(article.article.tagList, ['one','two','three']); 121 | assert.deepEqual(article.article.title, 'a title'); 122 | assert.deepEqual(article.article.author.username, 'testclient'); 123 | assert.deepEqual(article.article.author.following, false); 124 | 125 | }); 126 | 127 | it('Updates the author and checks for the change', async () => { 128 | let user2 = await client.service('users').find({query: {username: user.user.username}}); 129 | await client.service('user').update(user2.data[0]._id,{user: {bio: 'thebio', image: 'theimage'}}); 130 | 131 | article = await client.service('articles').get(article.article.slug); 132 | 133 | let slug = 'a-title_'; 134 | assert.deepEqual(article.article.slug.slice(0,slug.length),slug); 135 | assert.deepEqual(article.article.body,'abody'); 136 | assert.deepEqual(article.article.description, 'better description'); 137 | assert.deepEqual(article.article.favorited, false); 138 | assert.deepEqual(article.article.favoritesCount, 0); 139 | assert.deepEqual(article.article.tagList, ['one','two','three']); 140 | assert.deepEqual(article.article.title, 'a title'); 141 | assert.deepEqual(article.article.author.username, 'testclient'); 142 | assert.deepEqual(article.article.author.following, false); 143 | assert.deepEqual(article.article.author.bio, 'thebio'); 144 | assert.deepEqual(article.article.author.image, 'theimage'); 145 | 146 | }); 147 | 148 | it('Updates the article title and checks for the change to title and slug', async () => { 149 | article = await client.service('articles').update(article.article.slug,{article: {title: 'a new title'}}); 150 | 151 | article = await client.service('articles').get(article.article.slug); 152 | 153 | let slug = 'a-new-title_'; 154 | assert.deepEqual(article.article.slug.slice(0,slug.length),slug); 155 | assert.deepEqual(article.article.body,'abody'); 156 | assert.deepEqual(article.article.description, 'better description'); 157 | assert.deepEqual(article.article.favorited, false); 158 | assert.deepEqual(article.article.favoritesCount, 0); 159 | assert.deepEqual(article.article.tagList, ['one','two','three']); 160 | assert.deepEqual(article.article.title, 'a new title'); 161 | assert.deepEqual(article.article.author.username, 'testclient'); 162 | assert.deepEqual(article.article.author.following, false); 163 | 164 | }); 165 | 166 | it('cleans up', async () => { 167 | 168 | await client.service('articles').remove(article.article.slug); 169 | 170 | let user2 = await client.service('users').find({query: {username: user.user.username}}); 171 | await app.service('users').remove(user2.data[0]._id); 172 | 173 | }); 174 | }); 175 | }); 176 | 177 | async function makeClient() { 178 | var client = feathers(); 179 | // Configure the REST client to use 'node-fetch' 180 | const rest = feathersClient('http://localhost:3030'); 181 | client.configure(rest.fetch(fetch)); 182 | client.configure(auth({ 183 | path: '/users/login', 184 | storage: localStorage() 185 | })); 186 | // Make the authentication work 187 | client.service('/users/login').hooks({ 188 | after: { 189 | all(hook) { 190 | Object.assign(hook.result, { accessToken: `Token ${hook.result.user.token}` }); 191 | return Promise.resolve(hook); 192 | }, 193 | }, 194 | }); 195 | return client; 196 | } 197 | 198 | function localStorage () { 199 | const store = {}; 200 | 201 | return { 202 | setItem (key, value) { 203 | store[key] = value; 204 | }, 205 | getItem (key) { 206 | return store[key]; 207 | }, 208 | removeItem (key) { 209 | delete store[key]; 210 | } 211 | }; 212 | } 213 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |