Added graph API for assets

This commit is contained in:
Wyatt Johnson
2017-08-28 18:57:32 -06:00
parent df6f6a72ef
commit 90a8a87eaf
11 changed files with 342 additions and 36 deletions
+55
View File
@@ -0,0 +1,55 @@
const errors = require('../../errors');
const {
UPDATE_ASSET_SETTINGS,
UPDATE_ASSET_STATUS,
} = require('../../perms/constants');
const AssetsService = require('../../services/assets');
const AssetModel = require('../../models/asset');
/**
* updateSettings will update the settings on an asset.
*
* @param {Object} ctx graphql context
* @param {String} id the asset's id to update
* @param {Object} settings the settings to update on the asset.
*/
const updateSettings = async (ctx, id, settings) => AssetsService.overrideSettings(id, settings);
/**
* updateStatus will update the status of an asset.
*
* @param {Object} ctx graphql context
* @param {String} id the asset's id to update
* @param {Object} status the status to change on the asset relating to it's
* current state.
*/
const updateStatus = async (ctx, id, {closedAt, closedMessage}) => AssetModel.update({
id,
}, {
$set: {
closedAt,
closedMessage
}
});
module.exports = (ctx) => {
let mutators = {
Asset: {
updateSettings: () => Promise.reject(errors.ErrNotAuthorized),
updateStatus: () => Promise.reject(errors.ErrNotAuthorized)
}
};
if (ctx.user) {
if (ctx.user.can(UPDATE_ASSET_SETTINGS)) {
mutators.Asset.updateSettings = (id, settings) => updateSettings(ctx, id, settings);
}
if (ctx.user.can(UPDATE_ASSET_STATUS)) {
mutators.Asset.updateStatus = (id, status) => updateStatus(ctx, id, status);
}
}
return mutators;
};
+1 -1
View File
@@ -328,7 +328,7 @@ const createPublicComment = async (context, commentInput) => {
* @param {String} id identifier of the comment (uuid)
* @param {String} status the new status of the comment
*/
const setStatus = async ({user, loaders: {Comments}, pubsub}, {id, status}) => {
const setStatus = async ({user, loaders: {Comments}}, {id, status}) => {
let comment = await CommentsService.pushStatus(id, status, user ? user.id : null);
// If the loaders are present, clear the caches for these values because we
+2
View File
@@ -3,6 +3,7 @@ const debug = require('debug')('talk:graph:mutators');
const Comment = require('./comment');
const Action = require('./action');
const Asset = require('./asset');
const Tag = require('./tag');
const Token = require('./token');
const User = require('./user');
@@ -14,6 +15,7 @@ let mutators = [
// Load in the core mutators.
Comment,
Action,
Asset,
Tag,
Token,
User,
+6
View File
@@ -25,6 +25,12 @@ const RootMutation = {
rejectUsername(_, {input: {id, message}}, {mutators: {User}}) {
return wrapResponse(null)(User.rejectUsername({id, message}));
},
updateAssetSettings(_, {id, input: settings}, {mutators: {Asset}}) {
return wrapResponse(null)(Asset.updateSettings(id, settings));
},
updateAssetStatus(_, {id, input: status}, {mutators: {Asset}}) {
return wrapResponse(null)(Asset.updateStatus(id, status));
},
ignoreUser(_, {id}, {mutators: {User}}) {
return wrapResponse(null)(User.ignoreUser({id}));
},
+84 -1
View File
@@ -143,7 +143,19 @@ input AssetsQuery {
# Limit the number of results to be returned
limit: Int = 10
# open filters assets that are open/closed/all. Not providing this parameter
# will return all the assets, true will return assets that are open, and false
# will return assets that are closed.
open: Boolean
# sortOrder specifies the order of the sort for the returned Assets.
sortOrder: SORT_ORDER = DESC
# Skip results from the last created_at timestamp.
cursor: Cursor
}
################################################################################
## Tags
################################################################################
@@ -627,6 +639,22 @@ type Asset {
author: String
}
# AssetConnection represents a paginable subset of a asset list.
type AssetConnection {
# Indicates that there are more assets after this subset.
hasNextPage: Boolean!
# Cursor of first asset in subset.
startCursor: Date
# Cursor of last asset in subset.
endCursor: Date
# Subset of assets.
nodes: [Asset!]!
}
################################################################################
## Errors
################################################################################
@@ -714,7 +742,7 @@ type RootQuery {
comment(id: ID!): Comment
# All assets. Requires the `ADMIN` role.
assets(query: AssetsQuery): [Asset]
assets(query: AssetsQuery): AssetConnection
# Find or create an asset by url, or just find with the ID.
asset(id: ID, url: String): Asset
@@ -884,6 +912,54 @@ input RejectUsernameInput {
message: String!
}
# Configurable settings that can be overridden for the Asset.
input AssetSettingsInput {
# premodLinksEnable will put all comments that contain links into premod.
premodLinksEnable: Boolean
# moderation is the moderation mode for the asset.
moderation: MODERATION_MODE!
# questionBoxEnable will enable the Question Boxs' content to be visable above
# the comment box.
questionBoxEnable: Boolean
# questionBoxContent is the content of the Question Box.
questionBoxContent: String
# questionBoxIcon is the icon for the Question Box.
questionBoxIcon: String
}
# UpdateAssetStatusInput contains the input to change the status of a comment as
# it relates to being open/closed for commenting.
input UpdateAssetStatusInput {
# closedAt is the time that the asset will be closed for commenting. If this
# is null or in the future, it will be open for commenting.
closedAt: Date
# closedMessage is the message to be set on the asset when it is closed.
closedMessage: String
}
# UpdateAssetStatusResponse is the response returned with possibly some errors
# relating to the update status attempt.
type UpdateAssetStatusResponse implements Response {
# An array of errors relating to the mutation that occurred.
errors: [UserError!]
}
# UpdateAssetSettingsResponse is the response returned with possibly some errors
# relating to the update settings attempt.
type UpdateAssetSettingsResponse implements Response {
# An array of errors relating to the mutation that occurred.
errors: [UserError!]
}
# DeleteActionResponse is the response returned with possibly some errors
# relating to the delete action attempt.
type DeleteActionResponse implements Response {
@@ -1044,6 +1120,13 @@ type RootMutation {
# Removes a tag.
removeTag(tag: ModifyTagInput!): ModifyTagResponse!
# Updates settings on a given asset.
updateAssetSettings(id: ID!, input: AssetSettingsInput!): UpdateAssetSettingsResponse
# Updates the status of an asset allowing you to close/reopen an asset for
# commenting.
updateAssetStatus(id: ID!, input: UpdateAssetStatusInput!): UpdateAssetStatusResponse
# Ignore comments by another user
ignoreUser(id: ID!): IgnoreUserResponse
+2
View File
@@ -16,6 +16,8 @@ module.exports = {
UPDATE_CONFIG: 'UPDATE_CONFIG',
CREATE_TOKEN: 'CREATE_TOKEN',
REVOKE_TOKEN: 'REVOKE_TOKEN',
UPDATE_ASSET_SETTINGS: 'UPDATE_ASSET_SETTINGS',
UPDATE_ASSET_STATUS: 'UPDATE_ASSET_STATUS',
// queries
SEARCH_ASSETS: 'SEARCH_ASSETS',
+4
View File
@@ -33,6 +33,10 @@ module.exports = (user, perm) => {
return check(user, ['ADMIN']);
case types.REVOKE_TOKEN:
return check(user, ['ADMIN']);
case types.UPDATE_ASSET_SETTINGS:
return check(user, ['ADMIN', 'MODERATOR']);
case types.UPDATE_ASSET_STATUS:
return check(user, ['ADMIN', 'MODERATOR']);
default:
break;
}
-20
View File
@@ -1,7 +1,6 @@
const express = require('express');
const router = express.Router();
const scraper = require('../../../services/scraper');
const errors = require('../../../errors');
const AssetsService = require('../../../services/assets');
@@ -88,25 +87,6 @@ router.get('/:asset_id', async (req, res, next) => {
}
});
// Adds the asset id to the queue to be scraped.
router.post('/:asset_id/scrape', async (req, res, next) => {
try {
// Send back the asset.
let asset = await AssetsService.findById(req.params.asset_id);
if (!asset) {
return next(errors.ErrNotFound);
}
let job = await scraper.create(asset);
// Send the job back for monitoring.
res.status(201).json(job);
} catch (e) {
return next(e);
}
});
router.put('/:asset_id/settings', async (req, res, next) => {
try {
await AssetsService.overrideSettings(req.params.asset_id, req.body);
+51 -14
View File
@@ -108,19 +108,57 @@ module.exports = class AssetsService {
* @param {String} value string to search by.
* @return {Promise}
*/
static search({value, skip, limit} = {}) {
if (!value) {
return AssetsService.all(skip, limit);
} else {
return AssetModel
.find({
$text: {
$search: value
}
})
.skip(skip)
.limit(limit);
static search({value, limit, open, sortOrder, cursor} = {}) {
let assets = AssetModel.find({});
if (value) {
assets.merge({
$text: {
$search: value
}
});
}
if (open != null) {
if (open) {
assets.merge({
$or: [
{
closedAt: null
},
{
closedAt: {
$gt: Date.now()
}
}
]
});
} else {
assets.merge({
closedAt: {
$lt: Date.now()
}
});
}
}
if (cursor) {
if (sortOrder === 'DESC') {
assets.merge({
created_at: {
$lt: cursor,
},
});
} else {
assets.merge({
created_at: {
$gt: cursor,
},
});
}
}
return assets.limit(limit);
}
/**
@@ -185,10 +223,9 @@ module.exports = class AssetsService {
// That's it!
}
static all(skip = null, limit = null) {
static all(limit = undefined) {
return AssetModel
.find({})
.skip(skip)
.limit(limit);
}
};
@@ -0,0 +1,70 @@
const {graphql} = require('graphql');
const schema = require('../../../../graph/schema');
const Context = require('../../../../graph/context');
const UserModel = require('../../../../models/user');
const SettingsService = require('../../../../services/settings');
const AssetModel = require('../../../../models/asset');
const {expect} = require('chai');
describe('graph.mutations.updateAssetSettings', () => {
let asset;
beforeEach(async () => {
await SettingsService.init();
asset = await AssetModel.create({url: 'http://new.test.com/'});
});
const QUERY = `
mutation UpdateAssetStatus($id: ID!, $settings: AssetSettingsInput!) {
updateAssetSettings(id: $id, input: $settings) {
errors {
translation_key
}
}
}
`;
describe('context with different user roles', () => {
[
{error: 'NOT_AUTHORIZED'},
{roles: ['ADMIN', 'MODERATOR']},
{roles: ['MODERATOR']},
].forEach(({roles, error}) => {
it(roles ? roles.join(', ') : '<None>', async () => {
const user = new UserModel({roles});
const ctx = new Context({user});
const settings = {
premodLinksEnable: false,
moderation: 'POST',
questionBoxEnable: true,
questionBoxContent: 'Question?',
questionBoxIcon: '<Icon>',
};
const res = await graphql(schema, QUERY, {}, ctx, {
id: asset.id,
settings,
});
if (res.errors) {
console.error(res.errors);
}
expect(res.errors).to.be.empty;
if (error) {
expect(res.data.updateAssetSettings.errors).to.not.be.empty;
expect(res.data.updateAssetSettings.errors[0]).to.have.property('translation_key', error);
} else {
expect(res.data.updateAssetSettings.errors).to.be.null;
const retrievedAsset = await AssetModel.findOne({id: asset.id});
Object.keys(settings).forEach((key) => {
expect(retrievedAsset.settings).to.have.property(key, settings[key]);
});
}
});
});
});
});
@@ -0,0 +1,67 @@
const {graphql} = require('graphql');
const schema = require('../../../../graph/schema');
const Context = require('../../../../graph/context');
const UserModel = require('../../../../models/user');
const SettingsService = require('../../../../services/settings');
const AssetModel = require('../../../../models/asset');
const {expect} = require('chai');
describe('graph.mutations.updateAssetStatus', () => {
let asset;
beforeEach(async () => {
await SettingsService.init();
asset = await AssetModel.create({url: 'http://new.test.com/'});
});
const QUERY = `
mutation UpdateAssetStatus($id: ID!, $status: UpdateAssetStatusInput!) {
updateAssetStatus(id: $id, input: $status) {
errors {
translation_key
}
}
}
`;
describe('context with different user roles', () => {
[
{error: 'NOT_AUTHORIZED'},
{roles: ['ADMIN', 'MODERATOR']},
{roles: ['MODERATOR']},
].forEach(({roles, error}) => {
it(roles ? roles.join(', ') : '<None>', async () => {
const user = new UserModel({roles});
const ctx = new Context({user});
const closedAt = (new Date()).toISOString();
const closedMessage = 'my closed message!';
const res = await graphql(schema, QUERY, {}, ctx, {
id: asset.id,
status: {
closedAt,
closedMessage,
},
});
if (res.errors) {
console.error(res.errors);
}
expect(res.errors).to.be.empty;
if (error) {
expect(res.data.updateAssetStatus.errors).to.not.be.empty;
expect(res.data.updateAssetStatus.errors[0]).to.have.property('translation_key', error);
} else {
expect(res.data.updateAssetStatus.errors).to.be.null;
const retrievedAsset = await AssetModel.findOne({id: asset.id});
expect(retrievedAsset.closedAt).to.not.be.null;
expect(retrievedAsset).to.have.property('closedMessage', closedMessage);
}
});
});
});
});