diff --git a/.eslintignore b/.eslintignore index b051c6c57..6cb6a3bc3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ client +dist diff --git a/.eslintrc.json b/.eslintrc.json index eec982dac..5df3444a6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -45,6 +45,19 @@ "space-infix-ops": ["error"], "no-const-assign": [2], "no-duplicate-imports": [2], - "prefer-template": [1] + "prefer-template": [1], + "comma-spacing": [ + "error", + { + "after": true + } + ], + "no-var": [2], + "no-lonely-if": [2], + "curly": [2], + "no-multiple-empty-lines": [ + "error", + {"max": 1} + ] } } diff --git a/models/action.js b/models/action.js index cdbf92e8f..27c63747a 100644 --- a/models/action.js +++ b/models/action.js @@ -12,7 +12,7 @@ const ActionSchema = new Schema({ item_type: String, item_id: String, user_id: String -},{ +}, { timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' diff --git a/models/asset.js b/models/asset.js new file mode 100644 index 000000000..de8155131 --- /dev/null +++ b/models/asset.js @@ -0,0 +1,106 @@ +const mongoose = require('../mongoose'); +const uuid = require('uuid'); +const Schema = mongoose.Schema; + +const AssetSchema = new Schema({ + + id: { + type: String, + default: uuid.v4, + unique: true, + index: true + }, + url: { + type: String, + unique: true, + index: true + }, + type: { + type: String, + default: 'article' + }, + headline: String, + summary: String, + section: String, + subsection: String, + authors: [String], + publication_date: Date +}, { + versionKey: false, + timestamps: { + createdAt: 'created_at', + updatedAt: 'updated_at' + } +}); + +/** + * Search for assets. Currently only returns all. +*/ +AssetSchema.statics.search = function(query) { + + return Asset.find(query).exec(); + +}; + +/** + * Finds an asset by its id. + * @param {String} id identifier of the asset (uuid). +*/ +AssetSchema.statics.findById = function(id) { + + return Asset.findOne({id}).exec(); + +}; + +/** + * Finds a asset by its url. + * @param {String} url identifier of the asset (uuid). +*/ +AssetSchema.statics.findByUrl = function(url) { + + return Asset.findOne({'url': url}).exec(); + +}; + +/** + * Upserts an asset. +*/ +AssetSchema.statics.upsert = function(data) { + + // If an id is not sent, create one. + if (typeof data.id === 'undefined') { + data.id = uuid.v4(); + } + + // Perform the upsert against the id field. + let updatePromise = Asset.update({id: data.id}, data, {upsert: true}).exec() + .then(() => { + + // Pull the freshly minted asset out and return. + return Asset.findById(data.id); + + }) + .catch((err) => { + + console.error('Error upserting asset.', err); + //return new Promise(); // ??? what do we return on error? + + }); + + return updatePromise; + +}; + +/** + * Remove assets from the db. + * @param {String} query bson query to identify assets to be removed. +*/ +AssetSchema.statics.removeAll = function(query) { + + return Asset.remove(query).exec(); + +}; + +const Asset = mongoose.model('Asset', AssetSchema); + +module.exports = Asset; diff --git a/models/comment.js b/models/comment.js index 468478810..d6a814e64 100644 --- a/models/comment.js +++ b/models/comment.js @@ -21,7 +21,7 @@ const CommentSchema = new Schema({ default: '' }, parent_id: String -},{ +}, { timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' @@ -44,7 +44,6 @@ CommentSchema.statics.findByAssetId = function(asset_id) { return Comment.find({asset_id}); }; - const Comment = mongoose.model('Comment', CommentSchema); module.exports = Comment; diff --git a/models/user.js b/models/user.js index 76635abfb..4592ec847 100644 --- a/models/user.js +++ b/models/user.js @@ -11,7 +11,7 @@ const UserProfileSchema = new Schema({ }, display_name: String, auth_user_id: String -},{ +}, { timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' diff --git a/mongoose.js b/mongoose.js index 0aff4c22c..8d9bb3bfb 100644 --- a/mongoose.js +++ b/mongoose.js @@ -11,7 +11,7 @@ if (enabled('talk:db')) { try { mongoose.connect(url, (err) => { - if (err) throw err; + if (err) {throw err;} console.log('Connected to MongoDB!'); }); } catch (err) { diff --git a/package.json b/package.json index 17a6bda02..856db50c6 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "homepage": "https://github.com/coralproject/talk#readme", "dependencies": { "body-parser": "^1.15.2", + "chai-http": "^3.0.0", "debug": "^2.2.0", "express": "^4.14.0", "mongoose": "^4.6.5", diff --git a/routes/api/asset/index.js b/routes/api/asset/index.js new file mode 100644 index 000000000..b261da883 --- /dev/null +++ b/routes/api/asset/index.js @@ -0,0 +1,44 @@ +const express = require('express'); +const router = express.Router(); +const Asset = require('../../../models/asset'); + +// Search assets. +router.get('/', (req, res, next) => { + + let query = {}; + + if (typeof req.query.url !== 'undefined') { + query.url = req.query.url; + } + + Asset.search(query) + .then((asset) => { + res.json(asset); + }) + .catch(next); + +}); + +// Get an asset by id +router.get('/:id', (req, res, next) => { + + Asset.findById(req.params.id) + .then((asset) => { + res.json(asset); + }) + .catch(next); + +}); + +// Upsert an asset and return the affected document. +router.put('/', (req, res, next) => { + + Asset.upsert(req.body) + .then((asset) => { + res.json(asset); + }) + .catch(next); + +}); + +module.exports = router; diff --git a/routes/api/comments/index.js b/routes/api/comments/index.js index df9bbc456..212c47ac6 100644 --- a/routes/api/comments/index.js +++ b/routes/api/comments/index.js @@ -7,7 +7,6 @@ const router = express.Router(); // Routes //============================================================================== - router.get('/', (req, res) => { res.send('Read all of the comments ever'); }); diff --git a/routes/api/index.js b/routes/api/index.js index c7e5601ed..acd007dae 100644 --- a/routes/api/index.js +++ b/routes/api/index.js @@ -2,9 +2,10 @@ const express = require('express'); const router = express.Router(); +router.use('/asset', require('./asset')); router.use('/comments', require('./comments')); -router.use('/stream', require('./stream')); -router.use('/settings', require('./settings')); router.use('/queue', require('./queue')); +router.use('/settings', require('./settings')); +router.use('/stream', require('./stream')); module.exports = router; diff --git a/routes/api/stream/index.js b/routes/api/stream/index.js index c63bf871d..67216a252 100644 --- a/routes/api/stream/index.js +++ b/routes/api/stream/index.js @@ -17,7 +17,7 @@ router.get('/', (req, res, next) => { Action.findByItemIdArray(comments.map((comment) => comment.id)) ]); }).then(([comments, users, actions]) => { - res.json([...comments,...users,...actions]); + res.json([...comments, ...users, ...actions]); }).catch(error => { next(error); }); diff --git a/swagger.yaml b/swagger.yaml index 3ab5a21df..ba945e2e1 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -3,10 +3,10 @@ info: title: Talk API description: A commenting platform from The Coral Project. https://coralproject.net version: "0.0.1" -host: api.talk-coralproject.net +host: talk-stg.coralproject.net schemes: - https -basePath: /v1 +basePath: /api/v1 produces: - application/json paths: @@ -161,7 +161,35 @@ paths: description: Settings responses: 204: - description: Updated Setting + description: OK + /asset: + get: + tags: + - Asset + description: Get an asset by id. + responses: + 200: + description: OK + put: + tags: + - Asset + description: Upsert an asset. + responses: + 204: + description: OK + /asset?url={url}: + get: + tags: + - Asset + parameters: + - name: url + in: query + description: The url of the asset. + required: true + description: Get an asset by its url. + responses: + 200: + description: OK definitions: Item: @@ -214,11 +242,6 @@ definitions: type: string user_id: type: string - Setting: - type: object - properties: - id: - type: string moderation: type: string enum: @@ -232,3 +255,34 @@ definitions: type: string format: date-time description: Updated Date-Time + Asset: + type: object + properties: + id: + type: string + description: The uuid.v4 id of the asset. + url: + type: string + description: The url where the asset is found. + type: + type: string + description: What kind of asset it is. + default: article + headline: + type: string + description: The headline of the asset, inferred on initial load. + summary: + type: string + description: A summary of the asset, inferred on initial load. + section: + type: string + description: The section the asset is in. + subsection: + type: string + description: The subsection that the asset is in. + authors: + type: string + description: An array of the authors for this asset. + publication_date: + type: date + desctipion: When this asset was published. diff --git a/tests/.eslintrc.json b/tests/.eslintrc.json index 97d8c197e..363ef5a28 100644 --- a/tests/.eslintrc.json +++ b/tests/.eslintrc.json @@ -1,9 +1,10 @@ { "env": { "es6": true, - "node": true + "node": true, + "mocha": true }, - "extends": "eslint:recommended", + "extends": "../.eslintrc.json", "rules": { "no-undef": [0] } diff --git a/tests/index.js b/tests/index.js index 51ccc54ee..0c3d947ce 100644 --- a/tests/index.js +++ b/tests/index.js @@ -1,10 +1,5 @@ -/* eslint-env node, mocha */ -'use strict'; - -// require('./utils/mongoose') const expect = require('chai').expect; - describe('Comment', () => { describe('#add', () => { it('should add a comment', () => { diff --git a/tests/models/action.js b/tests/models/action.js index 8736edcab..34a9f2dca 100644 --- a/tests/models/action.js +++ b/tests/models/action.js @@ -1,19 +1,18 @@ -/* eslint-env node, mocha */ - require('../utils/mongoose'); + const Action = require('../../models/action'); const expect = require('chai').expect; describe('Action: models', () => { - var mockActions; + let mockActions; beforeEach(() => { return Action.create([{ action_type: 'flag', item_id: '123' - },{ + }, { action_type: 'like', item_id: '789' - },{ + }, { action_type: 'flag', item_id: '456' }]).then((actions) => { @@ -32,7 +31,7 @@ describe('Action: models', () => { describe('#findByItemIdArray()', () => { it('should find an array of actions from an array of item_ids', () => { - return Action.findByItemIdArray(['123','456']).then((result) => { + return Action.findByItemIdArray(['123', '456']).then((result) => { expect(result).to.have.length(2); }); }); diff --git a/tests/models/asset.js b/tests/models/asset.js new file mode 100644 index 000000000..b4d950a93 --- /dev/null +++ b/tests/models/asset.js @@ -0,0 +1,95 @@ +require('../utils/mongoose'); + +const chai = require('chai'); +const expect = chai.expect; +const server = require('../../app'); + +// Setup chai. +chai.should(); +chai.use(require('chai-http')); + +let fixture = { + 'url': 'http://hhgg.com/total-perspective-vortex', + 'type': 'article', + 'headline': 'The Total Perspective Vortex', + 'summary': 'You are an insignificant dot on an insignificant dot.', + 'section': 'Everything', + 'authors': ['Ford Prefect'] +}; + +describe('Asset: models', () => { + + describe('/GET Asset', () => { + describe('#get', () => { + it('It should get an empty array when there are no assets.', (done) => { + + chai.request(server) + .get('/api/v1/asset') + .end((err, res) => { + + if (err) { + throw new Error(err); + } + + res.should.have.status(200); + res.body.should.be.a('array'); + res.body.length.should.be.eql(0); + done(); + }); + + }); + }); + }); + + // This test checks PUT and read + describe('/PUT Asset', () => { + describe('#put', () => { + it('It should save an asset and load it again.', (done) => { + + chai.request(server) + .put('/api/v1/asset') + .send(fixture) + .end((err, res) => { + + if (err) { + throw new Error(err); + } + + res.should.have.status(200); + res.body.should.be.a('object'); + + // Id should be generated by the model if absent. + res.body.should.have.property('id'); + + // Save the asset id to compare with GET result. + let assetId = res.body.id; + + // Load the asset to make sure it's really there. + chai.request(server) + .get(`/api/v1/asset?url=${encodeURIComponent(fixture.url)}`) + .end((err, res) => { + + if (err) { + throw new Error(err); + } + + res.should.have.status(200); + res.body.should.be.an('array'); + + let asset = res.body[0]; + + expect(asset).to.have.property('id'); + + // Ensure the asset has the same id as above. + // This tests the single url per Id concept. + expect(assetId).to.equal(asset.id); + + done(); + + }); + }); + }); + }); + }); // End describe /PUT Asset + +}); diff --git a/tests/models/comment.js b/tests/models/comment.js index a8a543183..b3f0c0f30 100644 --- a/tests/models/comment.js +++ b/tests/models/comment.js @@ -1,20 +1,18 @@ -/* eslint-env node, mocha */ - require('../utils/mongoose'); const Comment = require('../../models/comment'); const expect = require('chai').expect; describe('Comment: models', () => { - var mockComments; + let mockComments; beforeEach(() => { return Comment.create([{ body: 'comment 10', asset_id: '123' - },{ + }, { body: 'comment 20', asset_id: '123' - },{ + }, { body: 'comment 30', asset_id: '456' }]).then((comments) => { @@ -35,7 +33,7 @@ describe('Comment: models', () => { it('should find an array of comments by asset id', () => { return Comment.findByAssetId('123').then((result) => { expect(result).to.have.length(2); - result.sort((a,b) => { + result.sort((a, b) => { if (a.body < b.body) {return -1;} else {return 1;} }); diff --git a/tests/models/user.js b/tests/models/user.js index e1063e21c..46fc7703c 100644 --- a/tests/models/user.js +++ b/tests/models/user.js @@ -1,17 +1,16 @@ -/* eslint-env node, mocha */ - require('../utils/mongoose'); + const User = require('../../models/user'); const expect = require('chai').expect; describe('User: models', () => { - var mockUsers; + let mockUsers; beforeEach(() => { return User.create([{ display_name: 'Stampi', - },{ + }, { display_name: 'Sockmonster', - },{ + }, { display_name: 'Marvel', }]).then((users) => { mockUsers = users; @@ -29,12 +28,10 @@ describe('User: models', () => { describe('#findByIdArray()', () => { it('should find an array of users from an array of ids', () => { - const ids = mockUsers.map((user) => user.id) + const ids = mockUsers.map((user) => user.id); return User.findByIdArray(ids).then((result) => { expect(result).to.have.length(3); }); }); }); - - // }); }); diff --git a/tests/routes/api/comments/index.js b/tests/routes/api/comments/index.js index ad21e54ec..1115a189b 100644 --- a/tests/routes/api/comments/index.js +++ b/tests/routes/api/comments/index.js @@ -4,10 +4,11 @@ require('../../../utils/mongoose'); const app = require('../../../../app'); const chai = require('chai'); -const chaiHttp = require('chai-http'); -chai.use(chaiHttp); -var expect = chai.expect; +const expect = chai.expect; +// Setup chai. +chai.should(); +chai.use(require('chai-http')); const Comment = require('../../../../models/comment'); const Action = require('../../../../models/action'); @@ -17,36 +18,36 @@ describe('Post /comments', () => { const users = [{ id: '123', display_name: 'John', - },{ + }, { id: '456', display_name: 'Paul', - }] + }]; const actions = [{ action_type: 'flag', item_id: 'abc' - },{ + }, { action_type: 'like', item_id: 'hij' - }] + }]; beforeEach(() => { return User.create(users).then(() => { - return Action.create(actions) - }) - }) + return Action.create(actions); + }); + }); it('it should create a comment', function(done) { chai.request(app) .post('/api/v1/comments') .query({'body': 'Something body.', 'author_id': '123', 'asset_id': '1', 'parent_id': ''}) .end(function(err, res){ - expect(res).to.have.status(201) - done() - }) - }) + expect(res).to.have.status(201); + done(); + }); + }); -}) +}); describe('Get /:comment_id', () => { const comments = [{ @@ -54,40 +55,40 @@ describe('Get /:comment_id', () => { body: 'comment 10', asset_id: 'asset', author_id: '123' - },{ + }, { id: 'def', body: 'comment 20', asset_id: 'asset', author_id: '456' - },{ + }, { id: 'hij', body: 'comment 30', asset_id: '456' - }] + }]; const users = [{ id: '123', display_name: 'John', - },{ + }, { id: '456', display_name: 'Paul', - }] + }]; const actions = [{ action_type: 'flag', item_id: 'abc' - },{ + }, { action_type: 'like', item_id: 'hij' - }] + }]; beforeEach(() => { return Comment.create(comments).then(() => { - return User.create(users) + return User.create(users); }).then(() => { - return Action.create(actions) - }) - }) + return Action.create(actions); + }); + }); it('should return the right comment for the comment_id', function(done){ chai.request(app) @@ -96,11 +97,8 @@ describe('Get /:comment_id', () => { .end(function(err, res){ expect(err).to.be.null; expect(res).to.have.status(200); - if (err) return done(err); + if (err) {return done(err);} done(); }); - }) - - - -}) + }); +}); diff --git a/tests/routes/api/stream/index.js b/tests/routes/api/stream/index.js index a4ca53228..9744abb0c 100644 --- a/tests/routes/api/stream/index.js +++ b/tests/routes/api/stream/index.js @@ -2,9 +2,11 @@ require('../../../utils/mongoose'); const app = require('../../../../app'); const chai = require('chai'); -const chaiHttp = require('chai-http'); -chai.use(chaiHttp); -var expect = chai.expect; +const expect = chai.expect; + +// Setup chai. +chai.should(); +chai.use(require('chai-http')); const Action = require('../../../../models/action'); const User = require('../../../../models/user'); @@ -16,40 +18,40 @@ describe('api/stream: routes', () => { body: 'comment 10', asset_id: 'asset', author_id: '123' - },{ + }, { id: 'def', body: 'comment 20', asset_id: 'asset', author_id: '456' - },{ + }, { id: 'hij', body: 'comment 30', asset_id: '456' - }] + }]; const users = [{ id: '123', display_name: 'John', - },{ + }, { id: '456', display_name: 'Paul', - }] + }]; const actions = [{ action_type: 'flag', item_id: 'abc' - },{ + }, { action_type: 'like', item_id: 'hij' - }] + }]; beforeEach(() => { return Comment.create(comments).then(() => { - return User.create(users) + return User.create(users); }).then(() => { - return Action.create(actions) - }) - }) + return Action.create(actions); + }); + }); it('should return a stream with comments, users and actions', function(done){ chai.request(app) @@ -58,8 +60,8 @@ describe('api/stream: routes', () => { .end(function(err, res){ expect(err).to.be.null; expect(res).to.have.status(200); - if (err) return done(err); + if (err) {return done(err);} done(); }); - }) -}) + }); +}); diff --git a/tests/utils/mongoose.js b/tests/utils/mongoose.js index 35083a4b3..1549440a6 100644 --- a/tests/utils/mongoose.js +++ b/tests/utils/mongoose.js @@ -1,4 +1,4 @@ -var mongoose = require('mongoose'); +const mongoose = require('../../mongoose'); // Ensure the NODE_ENV is set to 'test', // this is helpful when you would like to change behavior when testing. @@ -6,18 +6,18 @@ process.env.NODE_ENV = 'test'; beforeEach(function (done) { function clearDB() { - for (var i in mongoose.connection.collections) { + for (let i in mongoose.connection.collections) { mongoose.connection.collections[i].remove(function() {}); } return done(); } - if (mongoose.connection.readyState === 0) { - mongoose.connect('coral-talk-test', function (err) { + mongoose.on('open', function() { if (err) { throw err; } + return clearDB(); }); } else {