Tag server impl

This commit is contained in:
Wyatt Johnson
2017-05-19 12:23:53 -06:00
parent 132b0a854d
commit 8f219c47a7
10 changed files with 209 additions and 80 deletions
+24
View File
@@ -307,6 +307,30 @@ module.exports = {
}
```
#### Field: `tags`
The tags hook allows a plugin to define tags that are code controlled (added
or enabled by code). Below is an example pulled from the core off topic plugin
on how to create a hook for the `OFF_TOPIC` name:
```js
[
{
name: 'OFF_TOPIC',
permissions: {
public: true,
self: true,
roles: []
},
models: ['COMMENTS'],
created_at: new Date()
}
]
```
You can refer to `models/schema/tag.js` for the available schema to match when
creating models to enable/disable specific features.
#### Field: `passport`
```js
+2
View File
@@ -6,6 +6,7 @@ const Assets = require('./assets');
const Comments = require('./comments');
const Metrics = require('./metrics');
const Settings = require('./settings');
const Tags = require('./tags');
const Users = require('./users');
const plugins = require('../../services/plugins');
@@ -18,6 +19,7 @@ let loaders = [
Comments,
Metrics,
Settings,
Tags,
Users,
// Load the plugin loaders from the manager.
+22
View File
@@ -0,0 +1,22 @@
const DataLoader = require('dataloader');
const TagsService = require('../../services/tags');
/**
* Get all the tags for the context for the dataloader.
*/
const genAll = (context, queries) => {
return Promise.all(queries.map(({id, item_type, asset_id}) => {
return TagsService.getAll({id, item_type, asset_id});
}));
};
/**
* Creates a set of loaders based on a GraphQL context.
* @param {Object} context the context of the GraphQL request
* @return {Object} object of loaders
*/
module.exports = (context) => ({
Tags: {
getAll: new DataLoader((queries) => genAll(context, queries))
}
});
+55 -19
View File
@@ -2,11 +2,61 @@ const errors = require('../../errors');
const AssetsService = require('../../services/assets');
const ActionsService = require('../../services/actions');
const TagsService = require('../../services/tags');
const CommentsService = require('../../services/comments');
const linkify = require('linkify-it')();
const Wordlist = require('../../services/wordlist');
const debug = require('debug')('talk:graph:mutators:tags');
const plugins = require('../../services/plugins');
const pluginTags = plugins.get('server', 'tags').reduce((acc, {plugin, tags}) => {
debug(`added plugin '${plugin.name}'`);
acc = acc.concat(tags);
return acc;
}, []);
const resolveTagsForComment = async ({user, loaders: {Tags}}, {asset_id, tags = []}) => {
const item_type = 'COMMENTS';
// Handle Tags
if (tags.length) {
// Get the global list of tags from the dataloader.
let globalTags = await Tags.getAll.load({
item_type,
asset_id
});
if (!Array.isArray(globalTags)) {
globalTags = [];
}
globalTags = globalTags.concat(pluginTags);
// Merge in the tags for the given comment.
tags = tags.map((name) => {
// Resolve the TagLink that we can use for the comment.
let {tagLink} = TagsService.resolveLink(user, globalTags, {name, item_type});
// Return the tagLink for tag insertion.
return tagLink;
});
}
// Add the staff tag for comments created as a staff member.
if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) {
tags.push(TagsService.newTagLink(user, {
name: 'STAFF',
item_type
}));
}
return tags;
};
/**
* Creates a new comment.
* @param {Object} user the user performing the request
@@ -16,25 +66,11 @@ const Wordlist = require('../../services/wordlist');
* @param {String} [status='NONE'] the status of the new comment
* @return {Promise} resolves to the created comment
*/
const createComment = async ({user, loaders: {Comments}, pubsub}, {tags = [], body, asset_id, parent_id = null}, status = 'NONE') => {
const createComment = async (context, {tags = [], body, asset_id, parent_id = null}, status = 'NONE') => {
const {user, loaders: {Comments}, pubsub} = context;
// Handle Tags
if (tags.length) {
tags = tags.map((tag) => ({
tag: {
name: tag
}
}));
}
// Add the staff tag for comments created as a staff member.
if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) {
tags.push({
tag: {
name: 'STAFF'
}
});
}
// Resolve the tags for the comment.
tags = await resolveTagsForComment(context, {asset_id, tags});
let comment = await CommentsService.publicCreate({
body,
+8 -52
View File
@@ -4,62 +4,18 @@ const errors = require('../../errors');
/**
* Modifies the targeted model with the specified operation to add/remove a tag.
*/
const modify = async ({user}, operation, {name, id, item_type, asset_id}) => {
const modify = async ({user, loaders: {Tags}}, operation, {name, id, item_type, asset_id}) => {
// Try to find the tag in the global list. This will contain the permission
// information if it's found.
let tag = await TagsService.get({name, id, item_type, asset_id});
// Get the global list of tags from the dataloader.
const tags = await Tags.getAll.load({id, item_type, asset_id});
// Create the new tagLink that will be created to interact to the comment.
let tagLink = {
tag,
assigned_by: user.id,
created_at: new Date()
};
// If the tag was found, we need to ensure that the current user can indeed
// modify this tag on the comment.
if (tag) {
// If the tag has roles defined, and the current user has at least one of
// the required roles, then modify the tag without checking for ownership.
if (tag.permissions && tag.permissions.roles && tag.permissions.roles.some((role) => user.roles.include(role))) {
return operation(id, item_type, tagLink, false);
}
// If the permissions allow for self assignment, then ensure that the query
// is compose with that in mind.
if (tag.permissions && tag.permissions.self) {
// Otherwise, we assume that we have to check to see that the user indeed
// owns the resource before allowing the tag to get modified.
return operation(id, item_type, tagLink, true);
}
throw errors.ErrNotAuthorized;
}
// Only admin/moderators can modify unique tags, these are tags that are not
// in the global list.
if (!(user.hasRoles('ADMIN') || user.hasRoles('MODERATOR'))) {
throw errors.ErrNotAuthorized;
}
// Generate the tag in the event now that we have to create the tag for this
// specific comment.
tagLink.tag = {
name,
permissions: {
public: true,
self: false,
roles: []
},
models: [item_type],
created_at: new Date()
};
// Resolve the TagLink that should be used to insert to the user. This will
// addtionally return with an ownership property that can be used to determine
// that the user who adds this tag must also be the owner of the resource.
let {tagLink, ownership} = TagsService.resolveLink(user, tags, {name, item_type});
// Actually modify the tag on the model.
return operation(id, item_type, tagLink, false);
return operation(id, item_type, tagLink, ownership);
};
module.exports = (context) => {
+2 -1
View File
@@ -1,7 +1,8 @@
{
"server": [
"coral-plugin-respect",
"coral-plugin-facebook-auth"
"coral-plugin-facebook-auth",
"coral-plugin-offtopic"
],
"client": [
"coral-plugin-respect"
+12 -1
View File
@@ -2,5 +2,16 @@ const {readFileSync} = require('fs');
const path = require('path');
module.exports = {
typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8')
tags: [
{
name: 'OFF_TOPIC',
permissions: {
public: true,
self: true,
roles: []
},
models: ['COMMENTS'],
created_at: new Date()
}
]
};
@@ -1,4 +0,0 @@
## Extending TAG_TYPE by adding OFF_TOPIC Tag
enum TAG_TYPE {
OFF_TOPIC
}
+83 -2
View File
@@ -5,6 +5,8 @@ const UserModel = require('../models/user');
const AssetsService = require('./assets');
const SettingsService = require('./settings');
const errors = require('../errors');
const updateModel = async (item_type, query, update) => {
// Get the model to update with.
@@ -43,7 +45,7 @@ class TagsService {
/**
* Retrives a global tag from the settings based on the input_type.
*/
static async get({name, id, item_type, asset_id = null}) {
static async getAll({id, item_type, asset_id = null}) {
// Extract the settings from the database.
let settings;
@@ -66,7 +68,86 @@ class TagsService {
let {tags = []} = settings;
// Return the first tag that matches the requested form.
return tags.find((tag) => tag.name === name && tag.models.include(item_type));
return tags;
}
/**
* Resolves the tagLink and ownership verification requirements that should be
* used when trying to perform tag adding/removing operations.
*/
static resolveLink(user, tags, {name, item_type}) {
// Try to find the tag in the global list. This will contain the permission
// information if it's found.
let tag = tags.find((tag) => {
return tag.name === name && Array.isArray(tag.models) && tag.models.includes(item_type);
});
// Create the new tagLink that will be created to interact to the comment.
let tagLink = {
tag,
assigned_by: user.id,
created_at: new Date()
};
// If the tag was found, we need to ensure that the current user can indeed
// modify this tag on the comment.
if (tag) {
// If the tag has roles defined, and the current user has at least one of
// the required roles, then modify the tag without checking for ownership.
if (tag.permissions && tag.permissions.roles && tag.permissions.roles.some((role) => user.roles.include(role))) {
return {tagLink, ownership: false};
}
// If the permissions allow for self assignment, then ensure that the query
// is compose with that in mind.
if (tag.permissions && tag.permissions.self) {
// Otherwise, we assume that we have to check to see that the user indeed
// owns the resource before allowing the tag to get modified.
return {tagLink, ownership: true};
}
throw errors.ErrNotAuthorized;
}
// Only admin/moderators can modify unique tags, these are tags that are not
// in the global list.
if (!(user.hasRoles('ADMIN') || user.hasRoles('MODERATOR'))) {
throw errors.ErrNotAuthorized;
}
// Generate the tag in the event now that we have to create the tag for this
// specific comment.
tagLink = TagsService.newTagLink(user, {name, item_type});
// Actually modify the tag on the model.
return {tagLink, ownership: false};
}
static newTag({name, item_type}) {
return {
name,
permissions: {
public: true,
self: false,
roles: []
},
models: [item_type],
created_at: new Date()
};
}
/**
* Creates a new TagLink based on the input user and the tag data.
*/
static newTagLink(user, tag) {
return {
tag: TagsService.newTag(tag),
assigned_by: user.id,
created_at: new Date()
};
}
/**
+1 -1
View File
@@ -12,7 +12,7 @@ describe('graph.mutations.addTag', () => {
let comment, asset;
beforeEach(async () => {
await SettingsService.init();
asset = new AssetModel({url: 'http://new.test.com/'});
await asset.save();