mirror of
https://github.com/wassname/talk.git
synced 2026-06-29 23:09:26 +08:00
Merge pull request #522 from coralproject/142993479-tags
Get tags into a separate structure that can be applied to other models
This commit is contained in:
+24
@@ -342,6 +342,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
|
||||
|
||||
@@ -13,6 +13,7 @@ program
|
||||
.command('setup', 'setup the application')
|
||||
.command('jobs', 'work with the job queues')
|
||||
.command('users', 'work with the application auth')
|
||||
.command('migration', 'provides utilities for migrating the database')
|
||||
.command('plugins', 'provides utilities for interacting with the plugin system')
|
||||
.parse(process.argv);
|
||||
|
||||
|
||||
Executable
+101
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const util = require('./util');
|
||||
const inquirer = require('inquirer');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const MigrationService = require('../services/migration');
|
||||
|
||||
// Register shutdown hooks.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
async function createMigration(name) {
|
||||
try {
|
||||
|
||||
// Create the migration.
|
||||
await MigrationService.create(name);
|
||||
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function runMigrations() {
|
||||
|
||||
try {
|
||||
|
||||
let {backedUp} = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'backedUp',
|
||||
message: 'Did you perform a database backup',
|
||||
default: false
|
||||
}
|
||||
]);
|
||||
|
||||
if (!backedUp) {
|
||||
throw new Error('Please backup your databases prior to migrations occuring');
|
||||
}
|
||||
|
||||
// Get the migrations to run.
|
||||
let migrations = await MigrationService.listPending();
|
||||
|
||||
console.log('Now going to run the following migrations:\n');
|
||||
|
||||
for (let {filename} of migrations) {
|
||||
console.log(`\tmigrations/${filename}`);
|
||||
}
|
||||
|
||||
let {confirm} = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Proceed with migrations',
|
||||
default: false
|
||||
}
|
||||
]);
|
||||
|
||||
if (confirm) {
|
||||
|
||||
// Run the migrations.
|
||||
await MigrationService.run(migrations);
|
||||
} else {
|
||||
console.warn('Skipping migrations');
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('create <name>')
|
||||
.description('creates a new migration')
|
||||
.action(createMigration);
|
||||
|
||||
program
|
||||
.command('run')
|
||||
.description('runs all pending migrations')
|
||||
.action(runMigrations);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
if (process.argv.length <= 2) {
|
||||
program.outputHelp();
|
||||
util.shutdown();
|
||||
}
|
||||
+44
-1
@@ -2,9 +2,13 @@
|
||||
|
||||
const program = require('./commander');
|
||||
const app = require('../app');
|
||||
const debug = require('debug')('talk:cli:serve');
|
||||
const errors = require('../errors');
|
||||
const {createServer} = require('http');
|
||||
const scraper = require('../services/scraper');
|
||||
const mailer = require('../services/mailer');
|
||||
const MigrationService = require('../services/migration');
|
||||
const SetupService = require('../services/setup');
|
||||
const kue = require('../services/kue');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('./util');
|
||||
@@ -87,7 +91,46 @@ function onListening() {
|
||||
/**
|
||||
* Start the app.
|
||||
*/
|
||||
function startApp(program) {
|
||||
async function startApp(program) {
|
||||
|
||||
try {
|
||||
|
||||
// Check to see if the application is installed. If the application
|
||||
// has been installed, then it will throw errors.ErrSettingsNotInit, this
|
||||
// just means we don't have to check that the migrations have run.
|
||||
await SetupService.isAvailable();
|
||||
|
||||
debug('setup is currently available, migrations not being checked');
|
||||
|
||||
} catch (e) {
|
||||
|
||||
// Check the error.
|
||||
switch (e) {
|
||||
case errors.ErrInstallLock, errors.ErrSettingsInit:
|
||||
|
||||
debug('setup is not currently available, migrations now being checked');
|
||||
|
||||
// The error was expected, just continue.
|
||||
break;
|
||||
default:
|
||||
|
||||
// The error was not expected, throw the error!
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Now try and check the migration status.
|
||||
try {
|
||||
|
||||
// Verify that the minimum migration version is met.
|
||||
await MigrationService.verify();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
debug('migrations do not have to be run');
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
|
||||
+139
-149
@@ -8,6 +8,7 @@ const program = require('./commander');
|
||||
const inquirer = require('inquirer');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const SettingModel = require('../models/setting');
|
||||
const MODERATION_OPTIONS = require('../models/enum/moderation_options');
|
||||
const SettingsService = require('../services/settings');
|
||||
const SetupService = require('../services/setup');
|
||||
const UsersService = require('../services/users');
|
||||
@@ -32,166 +33,155 @@ program
|
||||
// Setup the application
|
||||
//==============================================================================
|
||||
|
||||
const performSetup = () => {
|
||||
|
||||
if (program.defaults) {
|
||||
return SettingsService
|
||||
.init()
|
||||
.then(() => {
|
||||
console.log('Settings created.');
|
||||
console.log('\nTalk is now installed!');
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
const performSetup = async () => {
|
||||
|
||||
// Get the current settings, we are expecing an error here.
|
||||
return SettingsService
|
||||
.retrieve()
|
||||
.then(() => {
|
||||
try {
|
||||
|
||||
// We should NOT have gotten a settings object, this means that the
|
||||
// application is already setup. Error out here.
|
||||
throw errors.ErrSettingsInit;
|
||||
// Try to get the settings.
|
||||
await SettingsService.retrieve();
|
||||
|
||||
})
|
||||
.catch((err) => {
|
||||
// We should NOT have gotten a settings object, this means that the
|
||||
// application is already setup. Error out here.
|
||||
throw errors.ErrSettingsInit;
|
||||
|
||||
// If the error is `not init`, then we're good, otherwise, it's something
|
||||
// else.
|
||||
if (err !== errors.ErrSettingsNotInit) {
|
||||
throw err;
|
||||
} catch (e) {
|
||||
|
||||
// If the error is `not init`, then we're good, otherwise, it's something
|
||||
// else.
|
||||
if (e !== errors.ErrSettingsNotInit) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (program.defaults) {
|
||||
await SettingsService.init();
|
||||
|
||||
console.log('Settings created.');
|
||||
console.log('\nTalk is now installed!');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the base settings model.
|
||||
let settings = new SettingModel();
|
||||
|
||||
console.log('\nWe\'ll ask you some questions in order to setup your installation of Talk.\n');
|
||||
|
||||
let answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'organizationName',
|
||||
message: 'Organization Name',
|
||||
default: settings.organizationName,
|
||||
validate: (input) => {
|
||||
if (input && input.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return 'Organization Name is required.';
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
choices: MODERATION_OPTIONS,
|
||||
name: 'moderation',
|
||||
default: settings.moderation,
|
||||
message: 'Select a moderation mode'
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'requireEmailConfirmation',
|
||||
default: settings.requireEmailConfirmation,
|
||||
message: 'Should emails always be confirmed'
|
||||
}
|
||||
]);
|
||||
|
||||
})
|
||||
.then(() => {
|
||||
// Update the settings that were changed.
|
||||
Object.keys(answers).forEach((key) => {
|
||||
if (answers[key] !== undefined) {
|
||||
settings[key] = answers[key];
|
||||
}
|
||||
});
|
||||
|
||||
// Create the base settings model.
|
||||
let settings = new SettingModel();
|
||||
console.log('\nWe\'ll ask you some questions about your first admin user.\n');
|
||||
|
||||
console.log('We\'ll ask you some questions in order to setup your installation of Talk.\n');
|
||||
|
||||
return inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'organizationName',
|
||||
message: 'Organization Name',
|
||||
default: settings.organizationName,
|
||||
validate: (input) => {
|
||||
if (input && input.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return 'Organization Name is required.';
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
choices: SettingModel.MODERATION_OPTIONS,
|
||||
name: 'moderation',
|
||||
default: settings.moderation,
|
||||
message: 'Select a moderation mode'
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'requireEmailConfirmation',
|
||||
default: settings.requireEmailConfirmation,
|
||||
message: 'Should emails always be confirmed'
|
||||
}
|
||||
])
|
||||
.then((answers) => {
|
||||
|
||||
// Update the settings that were changed.
|
||||
Object.keys(answers).forEach((key) => {
|
||||
if (answers[key] !== undefined) {
|
||||
settings[key] = answers[key];
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\nWe\'ll ask you some questions about your first admin user.\n');
|
||||
|
||||
return inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'username',
|
||||
message: 'Username',
|
||||
filter: (username) => {
|
||||
return UsersService
|
||||
.isValidUsername(username, false)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
message: 'Email',
|
||||
format: 'email',
|
||||
validate: (value) => {
|
||||
if (value && value.length >= 3) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return 'Email is required';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
filter: (password) => {
|
||||
return UsersService
|
||||
.isValidPassword(password)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
filter: (confirmPassword) => {
|
||||
return UsersService
|
||||
.isValidPassword(confirmPassword)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
]);
|
||||
})
|
||||
.then((user) => {
|
||||
|
||||
if (user.password !== user.confirmPassword) {
|
||||
return Promise.reject(new Error('Passwords do not match'));
|
||||
let user = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'username',
|
||||
message: 'Username',
|
||||
filter: (username) => {
|
||||
return UsersService
|
||||
.isValidUsername(username, false)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
message: 'Email',
|
||||
format: 'email',
|
||||
validate: (value) => {
|
||||
if (value && value.length >= 3) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return SetupService.setup({
|
||||
settings: settings.toObject(),
|
||||
user: {
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
password: user.password
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(({user}) => {
|
||||
console.log('Settings created.');
|
||||
console.log(`User ${user.id} created.`);
|
||||
console.log('\nTalk is now installed!');
|
||||
console.log('\nWe recommend adding TALK_INSTALL_LOCK=TRUE to your environment to turn off the dynamic setup.');
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
return 'Email is required';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
filter: (password) => {
|
||||
return UsersService
|
||||
.isValidPassword(password)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
filter: (confirmPassword) => {
|
||||
return UsersService
|
||||
.isValidPassword(confirmPassword)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
if (user.password !== user.confirmPassword) {
|
||||
return Promise.reject(new Error('Passwords do not match'));
|
||||
}
|
||||
|
||||
let {user: newUser} = await SetupService.setup({
|
||||
settings: settings.toObject(),
|
||||
user: {
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
password: user.password
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Settings created.');
|
||||
console.log(`User ${newUser.id} created.`);
|
||||
console.log('\nTalk is now installed!');
|
||||
console.log('\nWe recommend adding TALK_INSTALL_LOCK=TRUE to your environment to turn off the dynamic setup.');
|
||||
};
|
||||
|
||||
// Start tthe setup process.
|
||||
performSetup();
|
||||
performSetup()
|
||||
.then(() => {
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
});
|
||||
|
||||
@@ -26,8 +26,7 @@ const fm = new IntrospectionFragmentMatcher({
|
||||
{name: 'SetUserStatusResponse'},
|
||||
{name: 'SuspendUserResponse'},
|
||||
{name: 'SetCommentStatusResponse'},
|
||||
{name: 'AddCommentTagResponse'},
|
||||
{name: 'RemoveCommentTagResponse'},
|
||||
{name: 'ModifyTagResponse'},
|
||||
{name: 'IgnoreUserResponse'},
|
||||
{name: 'StopIgnoringUserResponse'}
|
||||
]
|
||||
|
||||
@@ -26,8 +26,8 @@ import {getActionSummary, iPerformedThisAction} from 'coral-framework/utils';
|
||||
import {getEditableUntilDate} from './util';
|
||||
import styles from './Comment.css';
|
||||
|
||||
const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF');
|
||||
const hasTag = (tags, lookupTag) => !!tags.filter((tag) => tag.name === lookupTag).length;
|
||||
const isStaff = (tags) => !tags.every((t) => t.tag.name !== 'STAFF');
|
||||
const hasTag = (tags, lookupTag) => !!tags.filter((t) => t.tag.name === lookupTag).length;
|
||||
const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
|
||||
|
||||
// resetCursors will return the id cursors of the first and second newest comment in
|
||||
@@ -180,10 +180,10 @@ export default class Comment extends React.Component {
|
||||
commentIsIgnored: React.PropTypes.func,
|
||||
|
||||
// dispatch action to add a tag to a comment
|
||||
addCommentTag: React.PropTypes.func,
|
||||
addTag: React.PropTypes.func,
|
||||
|
||||
// dispatch action to remove a tag from a comment
|
||||
removeCommentTag: React.PropTypes.func,
|
||||
removeTag: React.PropTypes.func,
|
||||
|
||||
// dispatch action to ignore another user
|
||||
ignoreUser: React.PropTypes.func,
|
||||
@@ -286,11 +286,11 @@ export default class Comment extends React.Component {
|
||||
deleteAction,
|
||||
disableReply,
|
||||
maxCharCount,
|
||||
addCommentTag,
|
||||
addNotification,
|
||||
charCountEnable,
|
||||
showSignInDialog,
|
||||
removeCommentTag,
|
||||
addTag,
|
||||
removeTag,
|
||||
liveUpdates,
|
||||
commentIsIgnored,
|
||||
commentClassNames = []
|
||||
@@ -333,18 +333,20 @@ export default class Comment extends React.Component {
|
||||
|
||||
const addBestTag = notifyOnError(
|
||||
() =>
|
||||
addCommentTag({
|
||||
addTag({
|
||||
id: comment.id,
|
||||
tag: BEST_TAG
|
||||
name: BEST_TAG,
|
||||
assetId: asset.id
|
||||
}),
|
||||
() => 'Failed to tag comment as best'
|
||||
);
|
||||
|
||||
const removeBestTag = notifyOnError(
|
||||
() =>
|
||||
removeCommentTag({
|
||||
removeTag({
|
||||
id: comment.id,
|
||||
tag: BEST_TAG
|
||||
name: BEST_TAG,
|
||||
assetId: asset.id
|
||||
}),
|
||||
() => 'Failed to remove best comment tag'
|
||||
);
|
||||
@@ -547,8 +549,8 @@ export default class Comment extends React.Component {
|
||||
currentUser={currentUser}
|
||||
postFlag={postFlag}
|
||||
deleteAction={deleteAction}
|
||||
addCommentTag={addCommentTag}
|
||||
removeCommentTag={removeCommentTag}
|
||||
addTag={addTag}
|
||||
removeTag={removeTag}
|
||||
ignoreUser={ignoreUser}
|
||||
charCountEnable={charCountEnable}
|
||||
maxCharCount={maxCharCount}
|
||||
|
||||
@@ -129,10 +129,10 @@ class Stream extends React.Component {
|
||||
postDontAgree,
|
||||
deleteAction,
|
||||
showSignInDialog,
|
||||
addCommentTag,
|
||||
addTag,
|
||||
ignoreUser,
|
||||
auth: {loggedIn, user},
|
||||
removeCommentTag,
|
||||
removeTag,
|
||||
pluginProps,
|
||||
editName
|
||||
} = this.props;
|
||||
@@ -264,8 +264,8 @@ class Stream extends React.Component {
|
||||
currentUser={user}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
addCommentTag={addCommentTag}
|
||||
removeCommentTag={removeCommentTag}
|
||||
addTag={addTag}
|
||||
removeTag={removeTag}
|
||||
ignoreUser={ignoreUser}
|
||||
commentIsIgnored={commentIsIgnored}
|
||||
loadMore={this.props.loadNewReplies}
|
||||
@@ -298,10 +298,10 @@ Stream.propTypes = {
|
||||
postComment: PropTypes.func.isRequired,
|
||||
|
||||
// dispatch action to add a tag to a comment
|
||||
addCommentTag: PropTypes.func,
|
||||
addTag: PropTypes.func,
|
||||
|
||||
// dispatch action to remove a tag from a comment
|
||||
removeCommentTag: PropTypes.func,
|
||||
removeTag: PropTypes.func,
|
||||
|
||||
// dispatch action to ignore another user
|
||||
ignoreUser: React.PropTypes.func,
|
||||
|
||||
@@ -28,7 +28,9 @@ export default withFragments({
|
||||
created_at
|
||||
status
|
||||
tags {
|
||||
name
|
||||
tag {
|
||||
name
|
||||
}
|
||||
}
|
||||
user {
|
||||
id
|
||||
|
||||
@@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux';
|
||||
import {ADDTL_COMMENTS_ON_LOAD_MORE} from '../constants/stream';
|
||||
import {
|
||||
withPostComment, withPostFlag, withPostDontAgree, withDeleteAction,
|
||||
withAddCommentTag, withRemoveCommentTag, withIgnoreUser, withEditComment,
|
||||
withAddTag, withRemoveTag, withIgnoreUser, withEditComment,
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
|
||||
import {notificationActions, authActions} from 'coral-framework';
|
||||
@@ -307,10 +307,9 @@ export default compose(
|
||||
withPostComment,
|
||||
withPostFlag,
|
||||
withPostDontAgree,
|
||||
withAddCommentTag,
|
||||
withRemoveCommentTag,
|
||||
withAddTag,
|
||||
withRemoveTag,
|
||||
withIgnoreUser,
|
||||
withDeleteAction,
|
||||
withEditComment,
|
||||
)(StreamContainer);
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ const extension = {
|
||||
}
|
||||
}
|
||||
`,
|
||||
RemoveCommentTagResponse: gql`
|
||||
fragment CoralEmbedStream_RemoveCommentTagResponse on RemoveCommentTagResponse {
|
||||
RemoveTagResponse: gql`
|
||||
fragment CoralEmbedStream_RemoveTagResponse on RemoveTagResponse {
|
||||
comment {
|
||||
id
|
||||
tags {
|
||||
@@ -28,8 +28,8 @@ const extension = {
|
||||
}
|
||||
}
|
||||
`,
|
||||
AddCommentTagResponse: gql`
|
||||
fragment CoralEmbedStream_AddCommentTagResponse on AddCommentTagResponse {
|
||||
AddTagResponse: gql`
|
||||
fragment CoralEmbedStream_AddTagResponse on AddTagResponse {
|
||||
comment {
|
||||
id
|
||||
tags {
|
||||
@@ -74,7 +74,13 @@ const extension = {
|
||||
status
|
||||
replyCount
|
||||
tags {
|
||||
name
|
||||
tag {
|
||||
name
|
||||
created_at
|
||||
}
|
||||
assigned_by {
|
||||
id
|
||||
}
|
||||
}
|
||||
user {
|
||||
id
|
||||
@@ -144,7 +150,18 @@ const extension = {
|
||||
parent_id,
|
||||
asset_id,
|
||||
action_summaries: [],
|
||||
tags: tags.map((t) => ({name: t, __typename: 'Tag'})),
|
||||
tags: tags.map((tag) => ({
|
||||
tag: {
|
||||
name: tag,
|
||||
created_at: new Date().toISOString(),
|
||||
__typename: 'Tag'
|
||||
},
|
||||
assigned_by: {
|
||||
id: auth.toJS().user.id,
|
||||
__typename: 'User'
|
||||
},
|
||||
__typename: 'TagLink'
|
||||
})),
|
||||
status: null,
|
||||
replyCount: 0,
|
||||
parent: parent_id
|
||||
|
||||
@@ -19,14 +19,29 @@ loadPluginsTranslations();
|
||||
injectPluginsReducers();
|
||||
injectReducers(reducers);
|
||||
|
||||
function inIframe() {
|
||||
try {
|
||||
return window.self !== window.top;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function init(config = {}) {
|
||||
store.dispatch(addExternalConfig(config));
|
||||
store.dispatch(checkLogin());
|
||||
}
|
||||
|
||||
// Don't run this in the popup.
|
||||
if (!window.opener) {
|
||||
pym.sendMessage('getConfig');
|
||||
|
||||
pym.onMessage('config', (config) => {
|
||||
store.dispatch(addExternalConfig(JSON.parse(config)));
|
||||
store.dispatch(checkLogin());
|
||||
});
|
||||
if (inIframe()) {
|
||||
pym.sendMessage('getConfig');
|
||||
pym.onMessage('config', (config) => {
|
||||
init(JSON.parse(config));
|
||||
});
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
}
|
||||
|
||||
render(
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import * as mutations from './mutations';
|
||||
|
||||
function createDefaultResponseFragments() {
|
||||
const names = Object.keys(mutations).map((key) => key.replace('with', ''));
|
||||
const result = {};
|
||||
names.forEach((name) => {
|
||||
const response = `${name}Response`;
|
||||
result[response] = gql`
|
||||
fragment Coral_${response} on ${response} {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
`;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
import {createDefaultResponseFragments} from '../utils';
|
||||
|
||||
// fragments defined here are automatically registered.
|
||||
export default {
|
||||
...createDefaultResponseFragments()
|
||||
...createDefaultResponseFragments(
|
||||
'SetCommentStatusResponse',
|
||||
'SuspendUserResponse',
|
||||
'RejectUsernameResponse',
|
||||
'SetUserStatusResponse',
|
||||
'PostCommentResponse',
|
||||
'EditCommentResponse',
|
||||
'PostFlagResponse',
|
||||
'CreateDontAgreeResponse',
|
||||
'DeleteActionResponse',
|
||||
'ModifyTagResponse',
|
||||
'IgnoreUserResponse',
|
||||
'StopIgnoringUserResponse',
|
||||
)
|
||||
};
|
||||
|
||||
@@ -173,39 +173,93 @@ export const withDeleteAction = withMutation(
|
||||
}}),
|
||||
});
|
||||
|
||||
export const withAddCommentTag = withMutation(
|
||||
const COMMENT_FRAGMENT = gql`
|
||||
fragment CoralBest_UpdateFragment on Comment {
|
||||
tags {
|
||||
tag {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const withAddTag = withMutation(
|
||||
gql`
|
||||
mutation AddCommentTag($id: ID!, $tag: String!) {
|
||||
addCommentTag(id:$id, tag:$tag) {
|
||||
...AddCommentTagResponse
|
||||
mutation AddTag($id: ID!, $asset_id: ID!, $name: String!) {
|
||||
addTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) {
|
||||
...ModifyTagResponse
|
||||
}
|
||||
}
|
||||
`, {
|
||||
props: ({mutate}) => ({
|
||||
addCommentTag: ({id, tag}) => {
|
||||
addTag: ({id, name, assetId}) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
id,
|
||||
tag
|
||||
}
|
||||
name,
|
||||
asset_id: assetId
|
||||
},
|
||||
optimisticResponse: {
|
||||
addTag: {
|
||||
__typename: 'ModifyTagResponse',
|
||||
errors: null,
|
||||
}
|
||||
},
|
||||
update: (proxy) => {
|
||||
const fragmentId = `Comment_${id}`;
|
||||
|
||||
// Read the data from our cache for this query.
|
||||
const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId});
|
||||
|
||||
data.tags.push({
|
||||
tag: {
|
||||
__typename: 'Tag',
|
||||
name: 'BEST'
|
||||
},
|
||||
__typename: 'TagLink'
|
||||
});
|
||||
|
||||
// Write our data back to the cache.
|
||||
proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data});
|
||||
},
|
||||
});
|
||||
}}),
|
||||
});
|
||||
|
||||
export const withRemoveCommentTag = withMutation(
|
||||
export const withRemoveTag = withMutation(
|
||||
gql`
|
||||
mutation RemoveCommentTag($id: ID!, $tag: String!) {
|
||||
removeCommentTag(id:$id, tag:$tag) {
|
||||
...RemoveCommentTagResponse
|
||||
mutation RemoveTag($id: ID!, $asset_id: ID!, $name: String!) {
|
||||
removeTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) {
|
||||
...ModifyTagResponse
|
||||
}
|
||||
}
|
||||
`, {
|
||||
props: ({mutate}) => ({
|
||||
removeCommentTag: ({id, tag}) => {
|
||||
removeTag: ({id, name, assetId}) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
id,
|
||||
tag
|
||||
name,
|
||||
asset_id: assetId
|
||||
},
|
||||
optimisticResponse: {
|
||||
removeTag: {
|
||||
__typename: 'ModifyTagResponse',
|
||||
errors: null,
|
||||
}
|
||||
},
|
||||
update: (proxy) => {
|
||||
const fragmentId = `Comment_${id}`;
|
||||
|
||||
// Read the data from our cache for this query.
|
||||
const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId});
|
||||
|
||||
const idx = data.tags.findIndex((i) => i.tag.name === 'BEST');
|
||||
|
||||
data.tags = [...data.tags.slice(0, idx), ...data.tags.slice(idx + 1)];
|
||||
|
||||
// Write our data back to the cache.
|
||||
proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data});
|
||||
}
|
||||
});
|
||||
}}),
|
||||
@@ -246,4 +300,3 @@ export const withStopIgnoringUser = withMutation(
|
||||
});
|
||||
}}),
|
||||
});
|
||||
|
||||
|
||||
@@ -112,3 +112,17 @@ export function getResponseErrors(mutationResult) {
|
||||
});
|
||||
return result.length ? result : false;
|
||||
}
|
||||
|
||||
export function createDefaultResponseFragments(...names) {
|
||||
const result = {};
|
||||
names.forEach((response) => {
|
||||
result[response] = gql`
|
||||
fragment Coral_${response} on ${response} {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
`;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,8 @@ import classnames from 'classnames';
|
||||
|
||||
// tag string for best comments
|
||||
export const BEST_TAG = 'BEST';
|
||||
export const commentIsBest = ({tags} = {}) => {
|
||||
const isBest = Array.isArray(tags) && tags.some((t) => t.name === BEST_TAG);
|
||||
return isBest;
|
||||
};
|
||||
|
||||
export const commentIsBest = ({tags} = {}) => tags.some((t) => t.tag.name === BEST_TAG);
|
||||
|
||||
const name = 'coral-plugin-best';
|
||||
|
||||
|
||||
@@ -7,3 +7,7 @@ export const removeTag = (idx) => ({
|
||||
type: 'REMOVE_TAG',
|
||||
idx
|
||||
});
|
||||
|
||||
export const clearTags = () => ({
|
||||
type: 'CLEAR_TAGS',
|
||||
});
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export const ADD_TAG = 'ADD_TAG';
|
||||
export const REMOVE_TAG = 'REMOVE_TAG';
|
||||
export const CLEAR_TAGS = 'CLEAR_TAGS';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ADD_TAG, REMOVE_TAG} from './constants';
|
||||
import {ADD_TAG, REMOVE_TAG, CLEAR_TAGS} from './constants';
|
||||
|
||||
const initialState = {
|
||||
tags: []
|
||||
@@ -19,6 +19,8 @@ export default function commentBox (state = initialState, action) {
|
||||
...state.tags.slice(action.idx + 1)
|
||||
]
|
||||
};
|
||||
case CLEAR_TAGS :
|
||||
return initialState;
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const SettingsService = require('../../services/settings');
|
||||
|
||||
const util = require('./util');
|
||||
|
||||
/**
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
});
|
||||
+56
-40
@@ -1,22 +1,70 @@
|
||||
const debug = require('debug')('talk:graph:mutators:comment');
|
||||
const errors = require('../../errors');
|
||||
|
||||
const ActionModel = require('../../models/action');
|
||||
const AssetsService = require('../../services/assets');
|
||||
const ActionsService = require('../../services/actions');
|
||||
const TagsService = require('../../services/tags');
|
||||
const CommentsService = require('../../services/comments');
|
||||
const KarmaService = require('../../services/karma');
|
||||
const linkify = require('linkify-it')();
|
||||
|
||||
const Wordlist = require('../../services/wordlist');
|
||||
const {
|
||||
CREATE_COMMENT,
|
||||
SET_COMMENT_STATUS,
|
||||
ADD_COMMENT_TAG,
|
||||
REMOVE_COMMENT_TAG,
|
||||
EDIT_COMMENT
|
||||
} = require('../../perms/constants');
|
||||
|
||||
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.can(ADD_COMMENT_TAG)) {
|
||||
tags.push(TagsService.newTagLink(user, {
|
||||
name: 'STAFF',
|
||||
item_type
|
||||
}));
|
||||
}
|
||||
|
||||
return tags;
|
||||
};
|
||||
|
||||
/**
|
||||
* adjustKarma will adjust the affected user's karma depending on the moderators
|
||||
* action.
|
||||
@@ -102,15 +150,11 @@ const adjustKarma = (Comments, id, status) => async () => {
|
||||
* @param {String} [status='NONE'] the status of the new comment
|
||||
* @return {Promise} resolves to the created comment
|
||||
*/
|
||||
const createComment = async ({user, loaders: {Comments}, pubsub}, {body, asset_id, parent_id = null, tags = []}, status = 'NONE') => {
|
||||
const createComment = async (context, {tags = [], body, asset_id, parent_id = null}, status = 'NONE') => {
|
||||
const {user, loaders: {Comments}, pubsub} = context;
|
||||
|
||||
// Building array of tags
|
||||
tags = tags.map((tag) => ({name: tag}));
|
||||
|
||||
// If admin or moderator, adding STAFF tag
|
||||
if (user.isStaff()) {
|
||||
tags.push({name: 'STAFF'});
|
||||
}
|
||||
// Resolve the tags for the comment.
|
||||
tags = await resolveTagsForComment(context, {asset_id, tags});
|
||||
|
||||
let comment = await CommentsService.publicCreate({
|
||||
body,
|
||||
@@ -305,24 +349,6 @@ const setStatus = async ({user, loaders: {Comments}}, {id, status}) => {
|
||||
return comment;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a tag to a Comment
|
||||
* @param {String} id identifier of the comment (uuid)
|
||||
* @param {String} tag name of the tag
|
||||
*/
|
||||
const addCommentTag = ({user, loaders: {Comments}}, {id, tag}) => {
|
||||
return CommentsService.addTag(id, tag, user.id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a tag from a Comment
|
||||
* @param {String} id identifier of the comment (uuid)
|
||||
* @param {String} tag name of the tag
|
||||
*/
|
||||
const removeCommentTag = ({user, loaders: {Comments}}, {id, tag}) => {
|
||||
return CommentsService.removeTag(id, tag);
|
||||
};
|
||||
|
||||
/**
|
||||
* Edit a Comment
|
||||
* @param {String} id identifier of the comment (uuid)
|
||||
@@ -354,9 +380,7 @@ module.exports = (context) => {
|
||||
Comment: {
|
||||
create: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
setStatus: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
addCommentTag: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
removeCommentTag: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
edit: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
edit: () => Promise.reject(errors.ErrNotAuthorized)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -368,14 +392,6 @@ module.exports = (context) => {
|
||||
mutators.Comment.setStatus = (action) => setStatus(context, action);
|
||||
}
|
||||
|
||||
if (context.user && context.user.can(ADD_COMMENT_TAG)) {
|
||||
mutators.Comment.addCommentTag = (action) => addCommentTag(context, action);
|
||||
}
|
||||
|
||||
if (context.user && context.user.can(REMOVE_COMMENT_TAG)) {
|
||||
mutators.Comment.removeCommentTag = (action) => removeCommentTag(context, action);
|
||||
}
|
||||
|
||||
if (context.user && context.user.can(EDIT_COMMENT)) {
|
||||
mutators.Comment.edit = (action) => edit(context, action);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ const debug = require('debug')('talk:graph:mutators');
|
||||
|
||||
const Comment = require('./comment');
|
||||
const Action = require('./action');
|
||||
const Tag = require('./tag');
|
||||
const User = require('./user');
|
||||
|
||||
const plugins = require('../../services/plugins');
|
||||
@@ -12,6 +13,7 @@ let mutators = [
|
||||
// Load in the core mutators.
|
||||
Comment,
|
||||
Action,
|
||||
Tag,
|
||||
User,
|
||||
|
||||
// Load the plugin mutators from the manager.
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
const TagsService = require('../../services/tags');
|
||||
const errors = require('../../errors');
|
||||
const {ADD_COMMENT_TAG, REMOVE_COMMENT_TAG} = require('../../perms/constants');
|
||||
|
||||
/**
|
||||
* Modifies the targeted model with the specified operation to add/remove a tag.
|
||||
*/
|
||||
const modify = async ({user, loaders: {Tags}}, operation, {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});
|
||||
|
||||
// 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, ownership);
|
||||
};
|
||||
|
||||
module.exports = (context) => {
|
||||
let mutators = {
|
||||
Tag: {
|
||||
add: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
remove: () => Promise.reject(errors.ErrNotAuthorized)
|
||||
}
|
||||
};
|
||||
|
||||
if (context.user && context.user.can(ADD_COMMENT_TAG)) {
|
||||
mutators.Tag.add = (tag) => modify(context, TagsService.add, tag);
|
||||
}
|
||||
|
||||
if (context.user && context.user.can(REMOVE_COMMENT_TAG)) {
|
||||
mutators.Tag.remove = (tag) => modify(context, TagsService.remove, tag);
|
||||
}
|
||||
|
||||
return mutators;
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
const {decorateWithTags} = require('./util');
|
||||
|
||||
const Asset = {
|
||||
recentComments({id}, _, {loaders: {Comments}}) {
|
||||
return Comments.genRecentComments.load(id);
|
||||
@@ -48,4 +50,7 @@ const Asset = {
|
||||
}
|
||||
};
|
||||
|
||||
// Decorate the Asset type resolver with a tags field.
|
||||
decorateWithTags(Asset);
|
||||
|
||||
module.exports = Asset;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const {decorateWithTags} = require('./util');
|
||||
|
||||
const Comment = {
|
||||
parent({parent_id}, _, {loaders: {Comments}}) {
|
||||
if (parent_id == null) {
|
||||
@@ -57,4 +59,7 @@ const Comment = {
|
||||
}
|
||||
};
|
||||
|
||||
// Decorate the Comment type resolver with a tags field.
|
||||
decorateWithTags(Comment);
|
||||
|
||||
module.exports = Comment;
|
||||
|
||||
@@ -16,6 +16,8 @@ const RootMutation = require('./root_mutation');
|
||||
const RootQuery = require('./root_query');
|
||||
const Settings = require('./settings');
|
||||
const Subscription = require('./subscription');
|
||||
const TagLink = require('./tag_link');
|
||||
const Tag = require('./tag');
|
||||
const UserError = require('./user_error');
|
||||
const User = require('./user');
|
||||
const ValidationUserError = require('./validation_user_error');
|
||||
@@ -39,6 +41,8 @@ let resolvers = {
|
||||
RootQuery,
|
||||
Settings,
|
||||
Subscription,
|
||||
TagLink,
|
||||
Tag,
|
||||
UserError,
|
||||
User,
|
||||
ValidationUserError,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const wrapResponse = require('../helpers/response');
|
||||
const CommentsService = require('../../services/comments');
|
||||
|
||||
const RootMutation = {
|
||||
createComment(_, {comment}, {mutators: {Comment}}) {
|
||||
@@ -35,12 +34,12 @@ const RootMutation = {
|
||||
setCommentStatus(_, {id, status}, {mutators: {Comment}}) {
|
||||
return wrapResponse(null)(Comment.setStatus({id, status}));
|
||||
},
|
||||
addCommentTag(_, {id, tag}, {mutators: {Comment}}) {
|
||||
return wrapResponse('comment')(Comment.addCommentTag({id, tag}).then(() => CommentsService.findById(id)));
|
||||
},
|
||||
removeCommentTag(_, {id, tag}, {mutators: {Comment}}) {
|
||||
return wrapResponse('comment')(Comment.removeCommentTag({id, tag}).then(() => CommentsService.findById(id)));
|
||||
addTag(_, {tag}, {mutators: {Tag}}) {
|
||||
return wrapResponse(null)(Tag.add(tag));
|
||||
},
|
||||
removeTag(_, {tag}, {mutators: {Tag}}) {
|
||||
return wrapResponse(null)(Tag.remove(tag));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = RootMutation;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
const Tag = {
|
||||
|
||||
};
|
||||
|
||||
module.exports = Tag;
|
||||
@@ -0,0 +1,11 @@
|
||||
const {SEARCH_OTHER_USERS} = require('../../perms/constants');
|
||||
|
||||
const TagLink = {
|
||||
assigned_by({assigned_by}, _, {user, loaders: {Users}}) {
|
||||
if (user && user.can(SEARCH_OTHER_USERS) && assigned_by != null) {
|
||||
return Users.getByID.load(assigned_by);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = TagLink;
|
||||
@@ -1,3 +1,4 @@
|
||||
const {decorateWithTags} = require('./util');
|
||||
const KarmaService = require('../../services/karma');
|
||||
const {
|
||||
SEARCH_ACTIONS,
|
||||
@@ -78,4 +79,7 @@ const User = {
|
||||
}
|
||||
};
|
||||
|
||||
// Decorate the User type resolver with a tags field.
|
||||
decorateWithTags(User);
|
||||
|
||||
module.exports = User;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
const {ADD_COMMENT_TAG} = require('../../perms/constants');
|
||||
|
||||
/**
|
||||
* Decorates the typeResolver with the tags field.
|
||||
*/
|
||||
const decorateWithTags = (typeResolver) => {
|
||||
typeResolver.tags = ({tags = []}, _, {user}) => {
|
||||
if (user && user.can(ADD_COMMENT_TAG)) {
|
||||
return tags;
|
||||
}
|
||||
|
||||
return tags.filter((t) => t.tag.permissions.public);
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
decorateWithTags
|
||||
};
|
||||
+96
-41
@@ -69,6 +69,9 @@ type User {
|
||||
# the current profiles of the user.
|
||||
profiles: [UserProfile]
|
||||
|
||||
# the tags on the user
|
||||
tags: [TagLink!]
|
||||
|
||||
# determines whether the user can edit their username
|
||||
canEditName: Boolean
|
||||
|
||||
@@ -87,17 +90,6 @@ type User {
|
||||
status: USER_STATUS
|
||||
}
|
||||
|
||||
type Tag {
|
||||
# the actual tag for the comment.
|
||||
name: String!
|
||||
|
||||
# the user that assigned the tag. If NULL then the system automatically tagged it.
|
||||
assigned_by: String
|
||||
|
||||
# the time when the tag was assigned.
|
||||
created_at: Date!
|
||||
}
|
||||
|
||||
# UsersQuery allows the ability to query users by a specific fields.
|
||||
input UsersQuery {
|
||||
action_type: ACTION_TYPE
|
||||
@@ -112,6 +104,51 @@ input UsersQuery {
|
||||
sort: SORT_ORDER = REVERSE_CHRONOLOGICAL
|
||||
}
|
||||
|
||||
|
||||
################################################################################
|
||||
## Tags
|
||||
################################################################################
|
||||
|
||||
# Used to represent the item type for a tag.
|
||||
enum TAGGABLE_ITEM_TYPE {
|
||||
|
||||
# The action references a entity of type Asset.
|
||||
ASSETS
|
||||
|
||||
# The action references a entity of type Comment.
|
||||
COMMENTS
|
||||
|
||||
# The action references a entity of type User.
|
||||
USERS
|
||||
}
|
||||
|
||||
# Tag represents the underlying Tag that can be either stored in a global list
|
||||
# or added uniquely to the entity.
|
||||
type Tag {
|
||||
|
||||
# The actual name of the tag entry.
|
||||
name: String!
|
||||
|
||||
# The time that this Tag was created.
|
||||
created_at: Date!
|
||||
}
|
||||
|
||||
# TagLink is used to associate a given Tag with a Model via a TagLink.
|
||||
type TagLink {
|
||||
|
||||
# The underlying Tag that is either duplicated from the global list or created
|
||||
# uniquely for this specific model.
|
||||
tag: Tag!
|
||||
|
||||
# The user that assigned the tag. This TagLink could have been created by the
|
||||
# system, in which case this will be null. It could also be null if the
|
||||
# current user is not an Admin/Moderator.
|
||||
assigned_by: User
|
||||
|
||||
# The date that the TagLink was created.
|
||||
created_at: Date!
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Comments
|
||||
################################################################################
|
||||
@@ -223,7 +260,7 @@ type Comment {
|
||||
body: String!
|
||||
|
||||
# the tags on the comment
|
||||
tags: [Tag]
|
||||
tags: [TagLink!]
|
||||
|
||||
# the user who authored the comment.
|
||||
user: User
|
||||
@@ -507,6 +544,9 @@ type Asset {
|
||||
# The date that the asset was created.
|
||||
created_at: Date
|
||||
|
||||
# the tags on the asset
|
||||
tags: [TagLink!]
|
||||
|
||||
# The author(s) of the asset.
|
||||
author: String
|
||||
}
|
||||
@@ -543,7 +583,7 @@ type ValidationUserError implements UserError {
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Queries;
|
||||
## Queries
|
||||
################################################################################
|
||||
|
||||
# Establishes the ordering of the content by their created_at time stamp.
|
||||
@@ -625,7 +665,7 @@ type RootQuery {
|
||||
interface Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError]
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# CreateCommentResponse is returned with the comment that was created and any
|
||||
@@ -636,7 +676,7 @@ type CreateCommentResponse implements Response {
|
||||
comment: Comment
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError]
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# Used to represent the item type for an action.
|
||||
@@ -652,8 +692,13 @@ enum ACTION_ITEM_TYPE {
|
||||
USERS
|
||||
}
|
||||
|
||||
enum TAG_TYPE {
|
||||
STAFF
|
||||
input CreateLikeInput {
|
||||
|
||||
# The item's id for which we are to create a like.
|
||||
item_id: ID!
|
||||
|
||||
# The type of the item for which we are to create the like.
|
||||
item_type: ACTION_ITEM_TYPE!
|
||||
}
|
||||
|
||||
# CreateCommentInput is the input content used to create a new comment.
|
||||
@@ -669,7 +714,7 @@ input CreateCommentInput {
|
||||
body: String!
|
||||
|
||||
# Tags
|
||||
tags: [TAG_TYPE]
|
||||
tags: [String]
|
||||
|
||||
}
|
||||
|
||||
@@ -697,7 +742,7 @@ type CreateFlagResponse implements Response {
|
||||
flag: FlagAction
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError]
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
|
||||
@@ -710,7 +755,7 @@ type CreateDontAgreeResponse implements Response {
|
||||
dontagree: DontAgreeAction
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError]
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
input CreateDontAgreeInput {
|
||||
@@ -756,7 +801,7 @@ input RejectUsernameInput {
|
||||
type DeleteActionResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError]
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# SetUserStatusResponse is the response returned with possibly some errors
|
||||
@@ -764,7 +809,7 @@ type DeleteActionResponse implements Response {
|
||||
type SetUserStatusResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError]
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# SuspendUserResponse is the response returned with possibly some errors
|
||||
@@ -772,7 +817,7 @@ type SetUserStatusResponse implements Response {
|
||||
type SuspendUserResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError]
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# RejectUsernameResponse is the response returned with possibly some errors
|
||||
@@ -780,7 +825,7 @@ type SuspendUserResponse implements Response {
|
||||
type RejectUsernameResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError]
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# SetCommentStatusResponse is the response returned with possibly some errors
|
||||
@@ -788,33 +833,43 @@ type RejectUsernameResponse implements Response {
|
||||
type SetCommentStatusResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError]
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# Response to addCommentTag mutation
|
||||
type AddCommentTagResponse implements Response {
|
||||
# An array of errors relating to the mutation that occured.
|
||||
comment: Comment
|
||||
errors: [UserError]
|
||||
# ModifyTagInput is the input used to modify a tag.
|
||||
input ModifyTagInput {
|
||||
|
||||
# name is the actual tag to add to the model.
|
||||
name: String!
|
||||
|
||||
# id is the ID of the model in question that we are modifying the tag of.
|
||||
id: ID!
|
||||
|
||||
# item_type is the type of item that we are modifying the tag if.
|
||||
item_type: TAGGABLE_ITEM_TYPE!
|
||||
|
||||
# asset_id is used when the item_type is `COMMENTS`, the is needed to rectify
|
||||
# the settings to get the asset specific tags/settings.
|
||||
asset_id: ID
|
||||
}
|
||||
|
||||
# Response to removeCommentTag mutation
|
||||
type RemoveCommentTagResponse implements Response {
|
||||
# Response to the addTag or removeTag mutations.
|
||||
type ModifyTagResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occured.
|
||||
comment: Comment
|
||||
errors: [UserError]
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# Response to ignoreUser mutation
|
||||
type IgnoreUserResponse implements Response {
|
||||
# An array of errors relating to the mutation that occured.
|
||||
errors: [UserError]
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# Response to stopIgnoringUser mutation
|
||||
type StopIgnoringUserResponse implements Response {
|
||||
# An array of errors relating to the mutation that occured.
|
||||
errors: [UserError]
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# Input to editComment mutation.
|
||||
@@ -831,7 +886,7 @@ type EditCommentResponse implements Response {
|
||||
comment: Comment
|
||||
|
||||
# An array of errors relating to the mutation that occured.
|
||||
errors: [UserError]
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# All mutations for the application are defined on this object.
|
||||
@@ -864,11 +919,11 @@ type RootMutation {
|
||||
# Sets Comment status. Requires the `ADMIN` role.
|
||||
setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse
|
||||
|
||||
# Add tag to comment.
|
||||
addCommentTag(id: ID!, tag: String!): AddCommentTagResponse
|
||||
# Add a tag.
|
||||
addTag(tag: ModifyTagInput!): ModifyTagResponse!
|
||||
|
||||
# Remove tag from comment.
|
||||
removeCommentTag(id: ID!, tag: String!): RemoveCommentTagResponse
|
||||
# Removes a tag.
|
||||
removeTag(tag: ModifyTagInput!): ModifyTagResponse!
|
||||
|
||||
# Ignore comments by another user
|
||||
ignoreUser(id: ID!): IgnoreUserResponse
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
const CommentModel = require('../models/comment');
|
||||
|
||||
module.exports = {
|
||||
async up() {
|
||||
|
||||
// Find all comments that have tags.
|
||||
let comments = await CommentModel.aggregate([
|
||||
{$match: {
|
||||
tags: {
|
||||
$exists: true,
|
||||
$ne: []
|
||||
}
|
||||
}},
|
||||
{$project: {
|
||||
id: true,
|
||||
tags: true
|
||||
}}
|
||||
]);
|
||||
|
||||
// If no comments were found, nothing needes to be done!
|
||||
if (comments.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new batch operation.
|
||||
let batch = CommentModel.collection.initializeUnorderedBulkOp();
|
||||
|
||||
// Loop over the comments retrieved, updating the tag structure.
|
||||
for (let {id, tags} of comments) {
|
||||
|
||||
// OLD
|
||||
//
|
||||
// [
|
||||
// {
|
||||
// name: 'OFF_TOPIC',
|
||||
// assigned_by: '',
|
||||
// created_at: new Date()
|
||||
// }
|
||||
// ]
|
||||
|
||||
// NEW
|
||||
//
|
||||
// [
|
||||
// {
|
||||
// tag: {
|
||||
// name: 'OFF_TOPIC',
|
||||
// permissions: {
|
||||
// public: true,
|
||||
// self: false,
|
||||
// roles: []
|
||||
// },
|
||||
// models: ['COMMENTS'],
|
||||
// created_at: new Date()
|
||||
// },
|
||||
// assigned_by: '',
|
||||
// created_at: new Date()
|
||||
// }
|
||||
// ]
|
||||
|
||||
// Remap the tag structure.
|
||||
tags = tags.map(({name, assigned_by, created_at}) => ({
|
||||
tag: {
|
||||
name,
|
||||
permissions: {
|
||||
public: true,
|
||||
self: name === 'OFF_TOPIC', // at the time of migration, only off topic tags were self assigning
|
||||
roles: []
|
||||
},
|
||||
models: ['COMMENTS'],
|
||||
created_at
|
||||
},
|
||||
assigned_by,
|
||||
created_at
|
||||
}));
|
||||
|
||||
// Execute the batch operation.
|
||||
batch.find({id}).updateOne({$set: {tags}});
|
||||
}
|
||||
|
||||
// Execute the batch update operation.
|
||||
await batch.execute();
|
||||
}
|
||||
};
|
||||
+2
-11
@@ -1,17 +1,8 @@
|
||||
const mongoose = require('../services/mongoose');
|
||||
const uuid = require('uuid');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const ACTION_TYPES = [
|
||||
'LIKE',
|
||||
'FLAG'
|
||||
];
|
||||
|
||||
const ITEM_TYPES = [
|
||||
'ASSETS',
|
||||
'COMMENTS',
|
||||
'USERS'
|
||||
];
|
||||
const ACTION_TYPES = require('./enum/action_types');
|
||||
const ITEM_TYPES = require('./enum/item_types');
|
||||
|
||||
const ActionSchema = new Schema({
|
||||
id: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const mongoose = require('../services/mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
const uuid = require('uuid');
|
||||
const TagLinkSchema = require('./schema/tag_link');
|
||||
|
||||
const AssetSchema = new Schema({
|
||||
id: {
|
||||
@@ -47,6 +48,9 @@ const AssetSchema = new Schema({
|
||||
default: null
|
||||
},
|
||||
|
||||
// Tags are added by the self or by administrators.
|
||||
tags: [TagLinkSchema],
|
||||
|
||||
// Additional metadata stored on the field.
|
||||
metadata: {
|
||||
default: {},
|
||||
|
||||
+15
-31
@@ -1,13 +1,8 @@
|
||||
const mongoose = require('../services/mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
const TagLinkSchema = require('./schema/tag_link');
|
||||
const uuid = require('uuid');
|
||||
|
||||
const STATUSES = [
|
||||
'ACCEPTED',
|
||||
'REJECTED',
|
||||
'PREMOD',
|
||||
'NONE'
|
||||
];
|
||||
const COMMENT_STATUS = require('./enum/comment_status');
|
||||
|
||||
/**
|
||||
* The Mongo schema for a Comment Status.
|
||||
@@ -16,7 +11,7 @@ const STATUSES = [
|
||||
const StatusSchema = new Schema({
|
||||
type: {
|
||||
type: String,
|
||||
enum: STATUSES,
|
||||
enum: COMMENT_STATUS,
|
||||
},
|
||||
|
||||
// The User ID of the user that assigned the status.
|
||||
@@ -30,27 +25,6 @@ const StatusSchema = new Schema({
|
||||
_id: false
|
||||
});
|
||||
|
||||
/**
|
||||
* The Mongo schema for a Comment Tag.
|
||||
* @type {Schema}
|
||||
*/
|
||||
const TagSchema = new Schema({
|
||||
name: String,
|
||||
|
||||
// The User ID of the user that assigned the status.
|
||||
assigned_by: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
created_at: {
|
||||
type: Date,
|
||||
default: Date
|
||||
}
|
||||
}, {
|
||||
_id: false
|
||||
});
|
||||
|
||||
/**
|
||||
* A record of old body values for a Comment
|
||||
*/
|
||||
@@ -89,12 +63,14 @@ const CommentSchema = new Schema({
|
||||
status_history: [StatusSchema],
|
||||
status: {
|
||||
type: String,
|
||||
enum: STATUSES,
|
||||
enum: COMMENT_STATUS,
|
||||
default: 'NONE'
|
||||
},
|
||||
tags: [TagSchema],
|
||||
parent_id: String,
|
||||
|
||||
// Tags are added by the self or by administrators.
|
||||
tags: [TagLinkSchema],
|
||||
|
||||
// Additional metadata stored on the field.
|
||||
metadata: {
|
||||
default: {},
|
||||
@@ -110,6 +86,14 @@ const CommentSchema = new Schema({
|
||||
},
|
||||
});
|
||||
|
||||
// Add the indexes for the id of the comment.
|
||||
CommentSchema.index({
|
||||
'id': 1
|
||||
}, {
|
||||
unique: true,
|
||||
background: false
|
||||
});
|
||||
|
||||
CommentSchema.virtual('edited').get(function() {
|
||||
return this.body_history.length > 1;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = [
|
||||
'LIKE',
|
||||
'FLAG'
|
||||
];
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = [
|
||||
'ACCEPTED',
|
||||
'REJECTED',
|
||||
'PREMOD',
|
||||
'NONE'
|
||||
];
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = [
|
||||
'ASSETS',
|
||||
'COMMENTS',
|
||||
'USERS'
|
||||
];
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = [
|
||||
'PRE',
|
||||
'POST'
|
||||
];
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = [
|
||||
'ADMIN',
|
||||
'MODERATOR',
|
||||
'STAFF'
|
||||
];
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = [
|
||||
'ACTIVE',
|
||||
'BANNED',
|
||||
'PENDING',
|
||||
'APPROVED' // Indicates that the users' username has been approved
|
||||
];
|
||||
@@ -0,0 +1,10 @@
|
||||
const mongoose = require('../services/mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const MigrationSchema = new Schema({
|
||||
version: Number
|
||||
});
|
||||
|
||||
const Migration = mongoose.model('Migration', MigrationSchema);
|
||||
|
||||
module.exports = Migration;
|
||||
@@ -0,0 +1,53 @@
|
||||
const mongoose = require('../../services/mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const ITEM_TYPES = require('../enum/item_types');
|
||||
const USER_ROLES = require('../enum/user_roles');
|
||||
|
||||
/**
|
||||
* The Mongo schema for a Tag.
|
||||
* @type {Schema}
|
||||
*/
|
||||
const TagSchema = new Schema({
|
||||
|
||||
// The actual name of the tag.
|
||||
name: String,
|
||||
|
||||
// Contains permission data.
|
||||
permissions: {
|
||||
|
||||
// Determines if this tag is public or not.
|
||||
public: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
// Determines if the owner of the Model can add/remove this tag on their own
|
||||
// resources.
|
||||
self: Boolean,
|
||||
|
||||
// Determines other roles that are allowed to set this tag on other
|
||||
// resources.
|
||||
roles: [{
|
||||
type: String,
|
||||
enum: USER_ROLES,
|
||||
default: []
|
||||
}]
|
||||
},
|
||||
|
||||
// A list of all the model types that this tag can be added to.
|
||||
models: [{
|
||||
type: String,
|
||||
enum: ITEM_TYPES
|
||||
}],
|
||||
|
||||
// The date for when the tag was created.
|
||||
created_at: {
|
||||
type: Date,
|
||||
default: Date
|
||||
}
|
||||
}, {
|
||||
_id: false
|
||||
});
|
||||
|
||||
module.exports = TagSchema;
|
||||
@@ -0,0 +1,31 @@
|
||||
const mongoose = require('../../services/mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
const TagSchema = require('./tag');
|
||||
|
||||
/**
|
||||
* The Mongo schema for linking a Tag to a Model.
|
||||
* @type {Schema}
|
||||
*/
|
||||
const TagLinkSchema = new Schema({
|
||||
|
||||
// A deep copy of the tag that is the origin for this link. If the ID matches
|
||||
// with existing tags in the global/asset context then content will be
|
||||
// substituted.
|
||||
tag: TagSchema,
|
||||
|
||||
// The User ID of the user that assigned the status.
|
||||
assigned_by: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
// The date when the tag was added to the model.
|
||||
created_at: {
|
||||
type: Date,
|
||||
default: Date
|
||||
}
|
||||
}, {
|
||||
_id: false
|
||||
});
|
||||
|
||||
module.exports = TagLinkSchema;
|
||||
+4
-7
@@ -1,10 +1,7 @@
|
||||
const mongoose = require('../services/mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const MODERATION_OPTIONS = [
|
||||
'PRE',
|
||||
'POST'
|
||||
];
|
||||
const TagSchema = require('./schema/tag');
|
||||
const MODERATION_OPTIONS = require('./enum/moderation_options');
|
||||
|
||||
/**
|
||||
* SettingSchema manages application settings that get used on front and backend.
|
||||
@@ -95,7 +92,8 @@ const SettingSchema = new Schema({
|
||||
type: Number,
|
||||
min: [0, 'Edit Comment Window length must be greater than zero'],
|
||||
default: 30 * 1000,
|
||||
}
|
||||
},
|
||||
tags: [TagSchema]
|
||||
}, {
|
||||
timestamps: {
|
||||
createdAt: 'created_at',
|
||||
@@ -127,4 +125,3 @@ SettingSchema.method('merge', function(src) {
|
||||
const Setting = mongoose.model('Setting', SettingSchema);
|
||||
|
||||
module.exports = Setting;
|
||||
module.exports.MODERATION_OPTIONS = MODERATION_OPTIONS;
|
||||
|
||||
+10
-16
@@ -1,27 +1,20 @@
|
||||
const mongoose = require('../services/mongoose');
|
||||
const bcrypt = require('bcrypt');
|
||||
const Schema = mongoose.Schema;
|
||||
const uuid = require('uuid');
|
||||
const TagLinkSchema = require('./schema/tag_link');
|
||||
const intersection = require('lodash/intersection');
|
||||
const can = require('../perms');
|
||||
|
||||
// USER_ROLES is the array of roles that is permissible as a user role.
|
||||
const USER_ROLES = [
|
||||
'ADMIN',
|
||||
'MODERATOR',
|
||||
'STAFF'
|
||||
];
|
||||
const USER_ROLES = require('./enum/user_roles');
|
||||
|
||||
// USER_STATUS is the list of statuses that are permitted for the user status.
|
||||
const USER_STATUS = [
|
||||
'ACTIVE',
|
||||
'BANNED',
|
||||
'PENDING',
|
||||
'APPROVED' // Indicates that the users' username has been approved
|
||||
];
|
||||
const USER_STATUS = require('./enum/user_status');
|
||||
|
||||
// ProfileSchema is the mongoose schema defined as the representation of a
|
||||
// User's profile stored in MongoDB.
|
||||
const ProfileSchema = new mongoose.Schema({
|
||||
const ProfileSchema = new Schema({
|
||||
|
||||
// ID provides the identifier for the user profile, in the case of a local
|
||||
// provider, the id would be an email, in the case of a social provider,
|
||||
@@ -44,7 +37,7 @@ const ProfileSchema = new mongoose.Schema({
|
||||
// used by the `local` provider to indicate when the email address was
|
||||
// confirmed.
|
||||
metadata: {
|
||||
type: mongoose.Schema.Types.Mixed
|
||||
type: Schema.Types.Mixed
|
||||
}
|
||||
}, {
|
||||
_id: false
|
||||
@@ -52,7 +45,7 @@ const ProfileSchema = new mongoose.Schema({
|
||||
|
||||
// UserSchema is the mongoose schema defined as the representation of a User in
|
||||
// MongoDB.
|
||||
const UserSchema = new mongoose.Schema({
|
||||
const UserSchema = new Schema({
|
||||
|
||||
// This ID represents the most unique identifier for a user, it is generated
|
||||
// when the user is created as a random uuid.
|
||||
@@ -136,6 +129,9 @@ const UserSchema = new mongoose.Schema({
|
||||
type: String,
|
||||
}],
|
||||
|
||||
// Tags are added by the self or by administrators.
|
||||
tags: [TagLinkSchema],
|
||||
|
||||
// Additional metadata stored on the field.
|
||||
metadata: {
|
||||
default: {},
|
||||
@@ -205,5 +201,3 @@ UserSchema.method('can', function(...actions) {
|
||||
const UserModel = mongoose.model('User', UserSchema);
|
||||
|
||||
module.exports = UserModel;
|
||||
module.exports.USER_ROLES = USER_ROLES;
|
||||
module.exports.USER_STATUS = USER_STATUS;
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
"embed-start": "NODE_ENV=development yarn build && ./bin/cli serve --jobs",
|
||||
"heroku-postbuild": "./bin/cli plugins reconcile && yarn build"
|
||||
},
|
||||
"talk": {
|
||||
"migration": {
|
||||
"minVersion": 1496771633
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"pre-git": {
|
||||
"commit-msg": [],
|
||||
@@ -110,6 +115,7 @@
|
||||
"resolve": "^1.3.2",
|
||||
"semver": "^5.3.0",
|
||||
"simplemde": "^1.11.2",
|
||||
"snake-case": "^2.1.0",
|
||||
"subscriptions-transport-ws": "^0.5.5-alpha.0",
|
||||
"timekeeper": "^1.0.0",
|
||||
"uuid": "^3.0.1",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"server": [
|
||||
"coral-plugin-respect",
|
||||
"coral-plugin-like",
|
||||
"coral-plugin-facebook-auth",
|
||||
"coral-plugin-auth",
|
||||
"coral-plugin-offtopic"
|
||||
"coral-plugin-like",
|
||||
"coral-plugin-respect",
|
||||
"coral-plugin-offtopic",
|
||||
"coral-plugin-facebook-auth"
|
||||
],
|
||||
"client": [
|
||||
"coral-plugin-respect",
|
||||
|
||||
@@ -60,7 +60,7 @@ type CreateLikeResponse implements Response {
|
||||
like: LikeAction
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError]
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
type RootMutation {
|
||||
|
||||
@@ -60,7 +60,7 @@ type CreateLoveResponse implements Response {
|
||||
love: LoveAction
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError]
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
type RootMutation {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import React from 'react';
|
||||
import styles from './styles.css';
|
||||
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
|
||||
const isOffTopic = (tags) => {
|
||||
return !!tags.filter((tag) => tag.name === 'OFF_TOPIC').length;
|
||||
};
|
||||
const isOffTopic = (tags) => !!tags.filter((t) => t.tag.name === 'OFF_TOPIC').length;
|
||||
|
||||
export default (props) => (
|
||||
<span>
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
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
|
||||
}
|
||||
@@ -44,7 +44,7 @@ type CreateRespectResponse implements Response {
|
||||
respect: RespectAction
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError]
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
type RootMutation {
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
const redis = require('./redis');
|
||||
const debug = require('debug')('talk:cache');
|
||||
const debug = require('debug')('talk:services:cache');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const cache = module.exports = {
|
||||
|
||||
@@ -108,78 +108,6 @@ module.exports = class CommentsService {
|
||||
return comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tag if it doesn't already exist on the comment.
|
||||
* @throws if tag is already added to the comment
|
||||
* @throws if tag name is not in ALLOWED_TAGS
|
||||
* @param {String} id the id of the comment to tag
|
||||
* @param {String} name the name of the tag to add
|
||||
* @param {String} assigned_by the user id for the user who added the tag
|
||||
*/
|
||||
static addTag(id, name, assigned_by) {
|
||||
|
||||
// Disabling allowed tags until we are able to extend them
|
||||
// if (ALLOWED_TAGS.find((t) => t.name === name) == null) {
|
||||
// return Promise.reject(new Error('tag not allowed'));
|
||||
// }
|
||||
|
||||
const filter = {
|
||||
id,
|
||||
'tags.name': {$ne: name},
|
||||
};
|
||||
const update = {
|
||||
$push: {tags: {
|
||||
name,
|
||||
assigned_by,
|
||||
created_at: new Date()
|
||||
}}
|
||||
};
|
||||
return CommentModel.update(filter, update)
|
||||
.then(({nModified}) => {
|
||||
switch (nModified) {
|
||||
case 0:
|
||||
|
||||
// either the tag was already there, or the comment doesn't exist with that id...
|
||||
throw new Error('Could not add tag to comment. Either the comment doesn\'t exist or the tag is already present.');
|
||||
case 1:
|
||||
|
||||
// tag added
|
||||
return;
|
||||
default:
|
||||
|
||||
// this should never happen because no multi parameter and unique index on id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a tag from a comment
|
||||
* @throws if the tag is not on the comment
|
||||
* @param {String} id the id of the comment to tag
|
||||
* @param {String} name the name of the tag to add
|
||||
*/
|
||||
static removeTag(id, name) {
|
||||
const filter = {
|
||||
id,
|
||||
'tags.name': name,
|
||||
};
|
||||
const update = {$pull: {tags: {name}}};
|
||||
return CommentModel.update(filter, update)
|
||||
.then(({nModified}) => {
|
||||
switch (nModified) {
|
||||
case 0:
|
||||
throw new Error('Could not remove tag from comment. Either the comment doesn\'t exist or the tag is not present');
|
||||
case 1:
|
||||
|
||||
// tag removed
|
||||
return;
|
||||
default:
|
||||
|
||||
// this should never happen because no multi parameter and unique index on id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a comment by the id.
|
||||
* @param {String} id identifier of comment (uuid)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const debug = require('debug')('talk:trust');
|
||||
const debug = require('debug')('talk:services:karma');
|
||||
const UserModel = require('../models/user');
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
const MigrationModel = require('../models/migration');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Joi = require('joi');
|
||||
const debug = require('debug')('talk:services:migration');
|
||||
const sc = require('snake-case');
|
||||
const {talk: {migration: {minVersion}}} = require('../package.json');
|
||||
|
||||
const migrationTemplate = `module.exports = {
|
||||
async up() {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
`;
|
||||
|
||||
class MigrationService {
|
||||
|
||||
/**
|
||||
* Creates a new migration file.
|
||||
*
|
||||
* @param {String} name name of the migration
|
||||
*/
|
||||
static async create(name) {
|
||||
if (!name || typeof name !== 'string' || name.length === 0) {
|
||||
throw new Error('name must be defined');
|
||||
}
|
||||
|
||||
// Create a new Migration based on the current time.
|
||||
let version = Math.round(Date.now() / 1000, 0);
|
||||
let filename = path.join(__dirname, '..', 'migrations', `${version}_${sc(name)}.js`);
|
||||
fs.writeFileSync(filename, migrationTemplate, 'utf8');
|
||||
|
||||
console.log(`Created migration ${version} in ${filename}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all pending migrations.
|
||||
*/
|
||||
static async listPending() {
|
||||
|
||||
// Get all the migration files.
|
||||
let migrationFiles = fs.readdirSync(path.join(__dirname, '..', 'migrations'));
|
||||
|
||||
// Ensure that all migrations follow this format.
|
||||
const migrationSchema = Joi.object({
|
||||
up: Joi.func().required(),
|
||||
down: Joi.func()
|
||||
});
|
||||
|
||||
// Extract the version from the filename with this regex.
|
||||
const versionRe = /(\d+)_([\S_]+)\.js/;
|
||||
|
||||
// Get the latest version.
|
||||
let latestVersion = await MigrationService.latestVersion();
|
||||
|
||||
// Parse the migrations from the file listing.
|
||||
let migrations = migrationFiles
|
||||
.map((filename) => {
|
||||
|
||||
// Parse the version from the filename.
|
||||
let matches = filename.match(versionRe);
|
||||
if (matches.length !== 3) {
|
||||
return null;
|
||||
}
|
||||
let version = parseInt(matches[1]);
|
||||
|
||||
// Don't rerun migrations from versions already ran.
|
||||
if (version <= latestVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read the migration from the filesystem.
|
||||
let migration = require(`../migrations/${filename}`);
|
||||
Joi.assert(migration, migrationSchema, `Migration ${filename} does did not pass validation`);
|
||||
|
||||
return {
|
||||
filename,
|
||||
version,
|
||||
migration
|
||||
};
|
||||
})
|
||||
.filter((migration) => migration !== null)
|
||||
.sort((a, b) => {
|
||||
if (a.version < b.version) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.version > b.version) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return migrations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs an list of migrations.
|
||||
*
|
||||
* @param {Array} migrations a list of migrations returned by `listPending`
|
||||
*/
|
||||
static async run(migrations) {
|
||||
if (migrations.length === 0) {
|
||||
console.log('No migrations to run!');
|
||||
return;
|
||||
}
|
||||
|
||||
for (let {filename, version, migration} of migrations) {
|
||||
try {
|
||||
console.log(`Starting migration ${filename}`);
|
||||
await migration.up();
|
||||
console.log(`Finished migration ${filename}`);
|
||||
} catch (e) {
|
||||
console.error(`Migration ${filename} failed`);
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Recording migration ${filename}`);
|
||||
|
||||
// Record that the migration was finished.
|
||||
let m = new MigrationModel({version});
|
||||
await m.save();
|
||||
|
||||
console.log(`Finished recording migration ${filename}`);
|
||||
} catch (e) {
|
||||
console.error(`Migration ${filename} could not be recorded`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Database now at migration version ${migrations[migrations.length - 1].version}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest migration version number that has been applied to the
|
||||
* database, null if none were found.
|
||||
*/
|
||||
static async latestVersion() {
|
||||
|
||||
// Load the latest migration details from the database.
|
||||
let latestMigration = await MigrationModel
|
||||
.find({})
|
||||
.sort({version: -1})
|
||||
.limit(1)
|
||||
.exec();
|
||||
|
||||
// If there weren't any migrations at all, then error out.
|
||||
if (latestMigration.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the latest migration does not match the required version, then error
|
||||
// out.
|
||||
return latestMigration[0].version;
|
||||
}
|
||||
|
||||
static async verify() {
|
||||
|
||||
// If the requiredVersion isn't specified or is 0, then don't complain.
|
||||
if (typeof minVersion !== 'number' || minVersion === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the latest migration does not match the required version, then error
|
||||
// out.
|
||||
let latestVersion = await MigrationService.latestVersion();
|
||||
if (!latestVersion || latestVersion < minVersion) {
|
||||
throw new Error(`A database migration is required, version required ${minVersion}, found ${latestVersion}. Please run \`./bin/cli migration run\``);
|
||||
}
|
||||
|
||||
debug(`minimum migration version ${minVersion} was met with version ${latestVersion}`);
|
||||
|
||||
return latestVersion;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MigrationService;
|
||||
@@ -59,3 +59,4 @@ require('../models/asset');
|
||||
require('../models/comment');
|
||||
require('../models/setting');
|
||||
require('../models/user');
|
||||
require('./migration');
|
||||
|
||||
@@ -7,7 +7,7 @@ const JWT = require('jsonwebtoken');
|
||||
const LocalStrategy = require('passport-local').Strategy;
|
||||
const errors = require('../errors');
|
||||
const uuid = require('uuid');
|
||||
const debug = require('debug')('talk:passport');
|
||||
const debug = require('debug')('talk:services:passport');
|
||||
const {createClient} = require('./redis');
|
||||
const bowser = require('bowser');
|
||||
const ms = require('ms');
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
const redis = require('redis');
|
||||
const debug = require('debug')('talk:redis');
|
||||
const debug = require('debug')('talk:services:redis');
|
||||
const {
|
||||
REDIS_URL
|
||||
} = require('../config');
|
||||
|
||||
+41
-43
@@ -1,5 +1,6 @@
|
||||
const UsersService = require('./users');
|
||||
const SettingsService = require('./settings');
|
||||
const MigrationService = require('./migration');
|
||||
const SettingsModel = require('../models/setting');
|
||||
const errors = require('../errors');
|
||||
const {
|
||||
@@ -15,34 +16,32 @@ module.exports = class SetupService {
|
||||
/**
|
||||
* This returns a promise which resolves if the setup is available.
|
||||
*/
|
||||
static isAvailable() {
|
||||
static async isAvailable() {
|
||||
|
||||
// Check if we have an install lock present.
|
||||
if (INSTALL_LOCK) {
|
||||
return Promise.reject(errors.ErrInstallLock);
|
||||
throw errors.ErrInstallLock;
|
||||
}
|
||||
|
||||
// Get the current settings, we are expecing an error here.
|
||||
return SettingsService
|
||||
.retrieve()
|
||||
.then(() => {
|
||||
try {
|
||||
|
||||
// We should NOT have gotten a settings object, this means that the
|
||||
// application is already setup. Error out here.
|
||||
return Promise.reject(errors.ErrSettingsInit);
|
||||
// Get the current settings, we are expecing an error here.
|
||||
await SettingsService.retrieve();
|
||||
|
||||
// We should NOT have gotten a settings object, this means that the
|
||||
// application is already setup. Error out here.
|
||||
throw errors.ErrSettingsInit;
|
||||
} catch (e) {
|
||||
|
||||
})
|
||||
.catch((err) => {
|
||||
// If the error is `not init`, then we're good, otherwise, it's something
|
||||
// else.
|
||||
if (e !== errors.ErrSettingsNotInit) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
// If the error is `not init`, then we're good, otherwise, it's something
|
||||
// else.
|
||||
if (err !== errors.ErrSettingsNotInit) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
// Allow the request to keep going here.
|
||||
return;
|
||||
});
|
||||
// Allow the request to keep going here.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,35 +68,34 @@ module.exports = class SetupService {
|
||||
/**
|
||||
* This will perform the setup.
|
||||
*/
|
||||
static setup({settings, user: {email, password, username}}) {
|
||||
static async setup({settings, user: {email, password, username}}) {
|
||||
|
||||
// Validate the settings first.
|
||||
return SetupService
|
||||
.validate({settings, user: {email, password, username}})
|
||||
.then(() => {
|
||||
return SettingsService.update(settings);
|
||||
})
|
||||
.then((settings) => {
|
||||
await SetupService.validate({settings, user: {email, password, username}});
|
||||
|
||||
// Settings are created! Create the user.
|
||||
// Get the migrations to run.
|
||||
let migrations = await MigrationService.listPending();
|
||||
|
||||
// Create the user.
|
||||
return UsersService
|
||||
.createLocalUser(email, password, username)
|
||||
// Perform all migrations.
|
||||
await MigrationService.run(migrations);
|
||||
|
||||
// Grant them administrative privileges and confirm the email account.
|
||||
.then((user) => {
|
||||
settings = await SettingsService.update(settings);
|
||||
|
||||
return Promise.all([
|
||||
UsersService.addRoleToUser(user.id, 'ADMIN'),
|
||||
UsersService.confirmEmail(user.id, email)
|
||||
])
|
||||
.then(() => ({
|
||||
settings,
|
||||
user
|
||||
}));
|
||||
});
|
||||
});
|
||||
// Settings are created! Create the user.
|
||||
|
||||
// Create the user.
|
||||
let user = await UsersService.createLocalUser(email, password, username);
|
||||
|
||||
// Grant them administrative privileges and confirm the email account.
|
||||
await Promise.all([
|
||||
UsersService.addRoleToUser(user.id, 'ADMIN'),
|
||||
UsersService.confirmEmail(user.id, email)
|
||||
]);
|
||||
|
||||
return {
|
||||
settings,
|
||||
user
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
const CommentModel = require('../models/comment');
|
||||
const AssetModel = require('../models/asset');
|
||||
const UserModel = require('../models/user');
|
||||
|
||||
const AssetsService = require('./assets');
|
||||
const SettingsService = require('./settings');
|
||||
const {ADD_COMMENT_TAG} = require('../perms/constants');
|
||||
|
||||
const errors = require('../errors');
|
||||
|
||||
const updateModel = async (item_type, query, update) => {
|
||||
|
||||
// Get the model to update with.
|
||||
let Model;
|
||||
switch (item_type) {
|
||||
case 'COMMENTS':
|
||||
Model = CommentModel;
|
||||
break;
|
||||
case 'ASSETS':
|
||||
Model = AssetModel;
|
||||
break;
|
||||
case 'USERS':
|
||||
Model = UserModel;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`item_type ${item_type} is not a valid item_type to update a tag on`);
|
||||
}
|
||||
|
||||
// Execute the update operation.
|
||||
return Model.update(query, update);
|
||||
};
|
||||
|
||||
const ownershipQuery = async (item_type, link, query) => {
|
||||
switch (item_type) {
|
||||
case 'COMMENTS':
|
||||
query['author_id'] = link.assigned_by;
|
||||
break;
|
||||
case 'USERS':
|
||||
query['id'] = link.assigned_by;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
class TagsService {
|
||||
|
||||
/**
|
||||
* Retrives a global tag from the settings based on the input_type.
|
||||
*/
|
||||
static async getAll({id, item_type, asset_id = null}) {
|
||||
|
||||
// Extract the settings from the database.
|
||||
let settings;
|
||||
switch (item_type) {
|
||||
case 'COMMENTS':
|
||||
settings = await AssetsService.rectifySettings(AssetsService.findById(asset_id));
|
||||
break;
|
||||
case 'ASSETS':
|
||||
settings = await AssetsService.rectifySettings(AssetsService.findById(id));
|
||||
break;
|
||||
case 'USERS':
|
||||
settings = await SettingsService.retrieve();
|
||||
break;
|
||||
default:
|
||||
settings = await SettingsService.retrieve();
|
||||
break;
|
||||
}
|
||||
|
||||
// Extract the tags from the settings object.
|
||||
let {tags = []} = settings;
|
||||
|
||||
// Return the first tag that matches the requested form.
|
||||
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.can(ADD_COMMENT_TAG)) {
|
||||
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()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a TagLink to a Model, optionally checking for ownership.
|
||||
*/
|
||||
static async add(id, item_type, link, ownershipCheck) {
|
||||
|
||||
// Compose the query to find the comment.
|
||||
const query = {
|
||||
id,
|
||||
'tags.tag.name': {
|
||||
$ne: link.tag.name
|
||||
}
|
||||
};
|
||||
|
||||
// If ownership verification is required, ensure that the person that is
|
||||
// assigning the tag is the same person that owns the comment.
|
||||
if (ownershipCheck) {
|
||||
|
||||
// Modify the query to support an ownership verification.
|
||||
ownershipQuery(item_type, link, query);
|
||||
}
|
||||
|
||||
// Get the Model to perform the update.
|
||||
return updateModel(item_type, query, {
|
||||
$push: {
|
||||
tags: link
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a TagLink to a Model, optionally checking for ownership.
|
||||
*/
|
||||
static async remove(id, item_type, link, ownershipCheck) {
|
||||
|
||||
// Compose the query to find the comment.
|
||||
const query = {
|
||||
id,
|
||||
'tags.tag.name': {
|
||||
$eq: link.tag.name
|
||||
}
|
||||
};
|
||||
|
||||
// If ownership verification is required, ensure that the person that is
|
||||
// assigning the tag is the same person that owns the comment.
|
||||
if (ownershipCheck) {
|
||||
|
||||
// Modify the query to support an ownership verification.
|
||||
ownershipQuery(item_type, link, query);
|
||||
}
|
||||
|
||||
// Get the Model to perform the update.
|
||||
return updateModel(item_type, query, {
|
||||
$pull: {
|
||||
tags: {
|
||||
name: link.tag.name
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TagsService;
|
||||
+2
-2
@@ -15,8 +15,8 @@ const redis = require('./redis');
|
||||
const redisClient = redis.createClient();
|
||||
|
||||
const UserModel = require('../models/user');
|
||||
const USER_STATUS = require('../models/user').USER_STATUS;
|
||||
const USER_ROLES = require('../models/user').USER_ROLES;
|
||||
const USER_STATUS = require('../models/enum/user_status');
|
||||
const USER_ROLES = require('../models/enum/user_roles');
|
||||
|
||||
const RECAPTCHA_WINDOW_SECONDS = 60 * 10; // 10 minutes.
|
||||
const RECAPTCHA_INCORRECT_TRIGGER = 5; // after 3 incorrect attempts, recaptcha will be required.
|
||||
|
||||
@@ -23,11 +23,11 @@ describe('graph.loaders.Metrics', () => {
|
||||
|
||||
describe('different comment states', () => {
|
||||
|
||||
beforeEach(() => CommentModel.create([
|
||||
{id: '1', body: 'a new comment!'},
|
||||
{id: '2', body: 'a new comment!'},
|
||||
{id: '3', body: 'a new comment!'}
|
||||
]));
|
||||
beforeEach(() =>[
|
||||
CommentModel.create({id: '1', body: 'a new comment!'}),
|
||||
CommentModel.create({id: '2', body: 'a new comment!'}),
|
||||
CommentModel.create({id: '3', body: 'a new comment!'})
|
||||
]);
|
||||
|
||||
[
|
||||
{flagged: 0, actions: []},
|
||||
@@ -52,7 +52,6 @@ describe('graph.loaders.Metrics', () => {
|
||||
to: (new Date()).setMinutes((new Date()).getMinutes() + 5)
|
||||
})
|
||||
.then(({data, errors}) => {
|
||||
console.log(errors);
|
||||
expect(errors).to.be.undefined;
|
||||
expect(data.flagged).to.have.length(flagged);
|
||||
});
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
const expect = require('chai').expect;
|
||||
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 CommentsService = require('../../../../services/comments');
|
||||
|
||||
describe('graph.mutations.addCommentTag', () => {
|
||||
let comment;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
comment = await CommentsService.publicCreate({body: `hello there! ${String(Math.random()).slice(2)}`});
|
||||
});
|
||||
|
||||
const query = `
|
||||
mutation AddCommentTag ($id: ID!, $tag: String!) {
|
||||
addCommentTag(id:$id, tag:$tag) {
|
||||
comment {
|
||||
tags {
|
||||
name
|
||||
}
|
||||
}
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
it('moderators can add tags to comments', async () => {
|
||||
const user = new UserModel({roles: ['MODERATOR' ]});
|
||||
const context = new Context({user});
|
||||
const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'});
|
||||
if (response.errors && response.errors.length) {
|
||||
console.error(response.errors);
|
||||
}
|
||||
expect(response.errors).to.be.empty;
|
||||
expect(response.data.addCommentTag.comment.tags).to.deep.equal([{name: 'BEST'}]);
|
||||
});
|
||||
|
||||
describe('users who cant add tags', () => {
|
||||
Object.entries({
|
||||
'anonymous': undefined,
|
||||
'regular commenter': new UserModel({}),
|
||||
'staff': new UserModel({roles: ['STAFF']}),
|
||||
'banned moderator': new UserModel({roles: ['MODERATOR'], status: 'BANNED'})
|
||||
}).forEach(([ userDescription, user ]) => {
|
||||
it(userDescription, async function () {
|
||||
const context = new Context({user});
|
||||
const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'});
|
||||
if (response.errors && response.errors.length) {
|
||||
console.error(response.errors);
|
||||
}
|
||||
expect(response.errors).to.be.empty;
|
||||
expect(response.data.addCommentTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]);
|
||||
expect(response.data.addCommentTag.comment).to.be.null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
const expect = require('chai').expect;
|
||||
const {graphql} = require('graphql');
|
||||
|
||||
const schema = require('../../../../graph/schema');
|
||||
const Context = require('../../../../graph/context');
|
||||
const AssetModel = require('../../../../models/asset');
|
||||
const UserModel = require('../../../../models/user');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const CommentsService = require('../../../../services/comments');
|
||||
|
||||
describe('graph.mutations.addTag', () => {
|
||||
let comment, asset;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
|
||||
asset = new AssetModel({url: 'http://new.test.com/'});
|
||||
await asset.save();
|
||||
|
||||
comment = await CommentsService.publicCreate({asset_id: asset.id, body: `hello there! ${String(Math.random()).slice(2)}`});
|
||||
});
|
||||
|
||||
const query = `
|
||||
mutation AddCommentTag ($id: ID!, $asset_id: ID!, $name: String!) {
|
||||
addTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
it('moderators can add tags to comments', async () => {
|
||||
const user = new UserModel({roles: ['MODERATOR']});
|
||||
const context = new Context({user});
|
||||
const res = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'}, 'AddCommentTag');
|
||||
if (res.errors && res.errors.length) {
|
||||
console.error(res.errors);
|
||||
}
|
||||
|
||||
console.log('res.errors', res.errors);
|
||||
expect(res.errors).to.be.empty;
|
||||
|
||||
let {tags} = await CommentsService.findById(comment.id);
|
||||
expect(tags).to.have.length(1);
|
||||
});
|
||||
|
||||
describe('users who cant add tags', () => {
|
||||
Object.entries({
|
||||
'anonymous': undefined,
|
||||
'regular commenter': new UserModel({}),
|
||||
'banned moderator': new UserModel({roles: ['MODERATOR'], status: 'BANNED'})
|
||||
}).forEach(([ userDescription, user ]) => {
|
||||
it(userDescription, async () => {
|
||||
const context = new Context({user});
|
||||
const res = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'}, 'AddCommentTag');
|
||||
if (res.errors && res.errors.length) {
|
||||
console.error(res.errors);
|
||||
}
|
||||
expect(res.errors).to.be.empty;
|
||||
expect(res.data.addTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -3,11 +3,14 @@ const {graphql} = require('graphql');
|
||||
|
||||
const schema = require('../../../../graph/schema');
|
||||
const Context = require('../../../../graph/context');
|
||||
|
||||
const UserModel = require('../../../../models/user');
|
||||
const AssetModel = require('../../../../models/asset');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const ActionModel = require('../../../../models/action');
|
||||
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const CommentsService = require('../../../../services/comments');
|
||||
|
||||
describe('graph.mutations.createComment', () => {
|
||||
beforeEach(() => SettingsService.init());
|
||||
|
||||
@@ -18,7 +21,9 @@ describe('graph.mutations.createComment', () => {
|
||||
id
|
||||
status
|
||||
tags {
|
||||
name
|
||||
tag {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
errors {
|
||||
@@ -220,11 +225,14 @@ describe('graph.mutations.createComment', () => {
|
||||
expect(data.createComment).to.have.property('comment').not.null;
|
||||
expect(data.createComment).to.have.property('errors').null;
|
||||
|
||||
return CommentsService.findById(data.createComment.comment.id);
|
||||
})
|
||||
.then(({tags}) => {
|
||||
if (tag) {
|
||||
expect(data.createComment.comment).to.have.property('tags').length(1);
|
||||
expect(data.createComment.comment.tags[0]).to.have.property('name', tag);
|
||||
expect(tags).to.have.length(1);
|
||||
expect(tags[0].tag.name).to.have.equal(tag);
|
||||
} else {
|
||||
expect(data.createComment.comment).to.have.property('tags').length(0);
|
||||
expect(tags).length(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+38
-20
@@ -4,24 +4,27 @@ const {graphql} = require('graphql');
|
||||
const schema = require('../../../../graph/schema');
|
||||
const Context = require('../../../../graph/context');
|
||||
const UserModel = require('../../../../models/user');
|
||||
const SettingModel = require('../../../../models/setting');
|
||||
|
||||
const AssetModel = require('../../../../models/asset');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const CommentsService = require('../../../../services/comments');
|
||||
const TagsService = require('../../../../services/tags');
|
||||
|
||||
describe('graph.mutations.removeCommentTag', () => {
|
||||
let comment;
|
||||
describe('graph.mutations.removeTag', () => {
|
||||
let asset, comment;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
comment = await CommentsService.publicCreate({body: `hello there! ${String(Math.random()).slice(2)}`});
|
||||
|
||||
asset = new AssetModel({url: 'http://new.test.com/'});
|
||||
await asset.save();
|
||||
|
||||
comment = await CommentsService.publicCreate({asset_id: asset.id, body: `hello there! ${String(Math.random()).slice(2)}`});
|
||||
});
|
||||
|
||||
const query = `
|
||||
mutation RemoveCommentTag ($id: ID!, $tag: String!) {
|
||||
removeCommentTag(id:$id, tag:$tag) {
|
||||
comment {
|
||||
tags {
|
||||
name
|
||||
}
|
||||
}
|
||||
mutation RemoveTag($id: ID!, $asset_id: ID!, $name: String!) {
|
||||
removeTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
@@ -30,38 +33,53 @@ describe('graph.mutations.removeCommentTag', () => {
|
||||
`;
|
||||
|
||||
it('moderators can add remove tags from comments', async () => {
|
||||
const user = new UserModel({roles: ['MODERATOR' ]});
|
||||
const user = new UserModel({roles: ['MODERATOR']});
|
||||
const context = new Context({user});
|
||||
|
||||
// add a tag first
|
||||
await CommentsService.addTag(comment.id, 'BEST');
|
||||
const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'});
|
||||
await TagsService.add(comment.id, 'COMMENTS', {tag: {name: 'BEST'}}, false);
|
||||
|
||||
const response = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'});
|
||||
if (response.errors && response.errors.length) {
|
||||
console.error(response.errors);
|
||||
}
|
||||
expect(response.errors).to.be.empty;
|
||||
expect(response.data.removeCommentTag.errors).to.be.null;
|
||||
expect(response.data.removeCommentTag.comment.tags).to.deep.equal([]);
|
||||
expect(response.data.removeTag.errors).to.be.null;
|
||||
|
||||
let retrievedComment = await CommentsService.findById(comment.id);
|
||||
|
||||
expect(retrievedComment.tags).to.have.length(0);
|
||||
});
|
||||
|
||||
describe('users who cant remove tags', () => {
|
||||
|
||||
before(() => SettingModel.findOneAndUpdate({id: 1}, {
|
||||
$push: {
|
||||
tags: {
|
||||
id: 'BEST',
|
||||
models: ['COMMENTS']
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
Object.entries({
|
||||
'anonymous': undefined,
|
||||
'regular commenter': new UserModel({}),
|
||||
'banned moderator': new UserModel({roles: ['MODERATOR'], status: 'BANNED'})
|
||||
}).forEach(([ userDescription, user ]) => {
|
||||
}).forEach(([userDescription, user]) => {
|
||||
it(userDescription, async function () {
|
||||
const context = new Context({user});
|
||||
|
||||
// add a tag first
|
||||
await CommentsService.addTag(comment.id, 'BEST');
|
||||
const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'});
|
||||
await TagsService.add(comment.id, 'COMMENTS', {tag: {name: 'BEST'}}, false);
|
||||
|
||||
const response = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'});
|
||||
if (response.errors && response.errors.length) {
|
||||
console.error(response.errors);
|
||||
}
|
||||
expect(response.errors).to.be.empty;
|
||||
expect(response.data.removeCommentTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]);
|
||||
expect(response.data.removeCommentTag.comment).to.be.null;
|
||||
|
||||
expect(response.data.removeTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -219,64 +219,6 @@ describe('services.CommentsService', () => {
|
||||
|
||||
});
|
||||
|
||||
describe('#addTag', () => {
|
||||
it('adds a tag', async () => {
|
||||
const commentId = comments[0].id;
|
||||
const tagName = 'BEST';
|
||||
const userId = users[0].id;
|
||||
await CommentsService.addTag(commentId, tagName, userId);
|
||||
const updatedComment = await CommentsService.findById(commentId);
|
||||
expect(updatedComment.tags.length).to.equal(1);
|
||||
expect(updatedComment.tags[0].name).to.equal(tagName);
|
||||
expect(updatedComment.tags[0].assigned_by).to.equal(userId);
|
||||
expect(updatedComment.tags[0].created_at).to.be.an.instanceof(Date);
|
||||
});
|
||||
it('can\'t add a tag to comment id that doesn\'t exist', async () => {
|
||||
const commentId = 'fakenews';
|
||||
const tagName = 'BEST';
|
||||
const userId = users[0].id;
|
||||
await expect(CommentsService.addTag(commentId, tagName, userId)).to.be.rejected;
|
||||
});
|
||||
it('can\'t add same tag.name twice', async () => {
|
||||
const commentId = comments[0].id;
|
||||
const tagName = 'BEST';
|
||||
const userId = users[0].id;
|
||||
|
||||
// first time
|
||||
await CommentsService.addTag(commentId, tagName, userId);
|
||||
|
||||
// second time should fail
|
||||
await expect(CommentsService.addTag(commentId, tagName, userId)).to.be.rejected;
|
||||
});
|
||||
});
|
||||
|
||||
describe('#removeTag', () => {
|
||||
it('removes a tag', async () => {
|
||||
const commentId = comments[0].id;
|
||||
const tagName = 'BEST';
|
||||
await CommentsService.addTag(commentId, tagName, users[0].id);
|
||||
const updatedComment = await CommentsService.findById(commentId);
|
||||
expect(updatedComment.tags.length).to.equal(1);
|
||||
|
||||
// ok now to remove it
|
||||
await CommentsService.removeTag(commentId, tagName);
|
||||
const updatedComment2 = await CommentsService.findById(commentId);
|
||||
expect(updatedComment2.tags.length).to.equal(0);
|
||||
});
|
||||
it('throws if removing a tag that isn\'t there', async () => {
|
||||
const commentId = comments[0].id;
|
||||
|
||||
// just make sure it has no tags to start
|
||||
const updatedComment2 = await CommentsService.findById(commentId);
|
||||
expect(updatedComment2.tags.length).to.equal(0);
|
||||
|
||||
const tagName = 'BEST';
|
||||
|
||||
// ok now to remove it
|
||||
await expect(CommentsService.removeTag(commentId, tagName)).to.be.rejected;
|
||||
});
|
||||
});
|
||||
|
||||
describe('#changeStatus', () => {
|
||||
|
||||
it('should change the status of a comment from no status', () => {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
const CommentsService = require('../../../services/comments');
|
||||
const TagsService = require('../../../services/tags');
|
||||
const UsersService = require('../../../services/users');
|
||||
const SettingsService = require('../../../services/settings');
|
||||
|
||||
const CommentModel = require('../../../models/comment');
|
||||
|
||||
const expect = require('chai').use(require('chai-as-promised')).expect;
|
||||
|
||||
describe('services.TagsService', () => {
|
||||
let comment, user;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
user = await UsersService.createLocalUser('stampi@gmail.com', '1Coral!!', 'Stampi');
|
||||
comment = await CommentModel.create({
|
||||
id: '1',
|
||||
body: 'comment 10',
|
||||
asset_id: '123',
|
||||
status_history: [],
|
||||
parent_id: null,
|
||||
author_id: user.id
|
||||
});
|
||||
});
|
||||
|
||||
describe('#add', () => {
|
||||
it('adds a tag', async () => {
|
||||
const id = comment.id;
|
||||
const name = 'BEST';
|
||||
const assigned_by = user.id;
|
||||
|
||||
await TagsService.add(id, 'COMMENTS', {
|
||||
tag: {
|
||||
name
|
||||
},
|
||||
assigned_by
|
||||
});
|
||||
|
||||
const {tags} = await CommentsService.findById(id);
|
||||
expect(tags.length).to.equal(1);
|
||||
expect(tags[0].tag.name).to.equal(name);
|
||||
expect(tags[0].assigned_by).to.equal(assigned_by);
|
||||
});
|
||||
|
||||
it('can\'t add same tag.id twice', async () => {
|
||||
const id = comment.id;
|
||||
const name = 'BEST';
|
||||
const assigned_by = user.id;
|
||||
|
||||
await TagsService.add(id, 'COMMENTS', {
|
||||
tag: {
|
||||
name
|
||||
},
|
||||
assigned_by
|
||||
});
|
||||
|
||||
{
|
||||
let {tags} = await CommentsService.findById(id);
|
||||
expect(tags.length).to.equal(1);
|
||||
}
|
||||
|
||||
await TagsService.add(id, 'COMMENTS', {
|
||||
tag: {
|
||||
name
|
||||
},
|
||||
assigned_by
|
||||
});
|
||||
|
||||
{
|
||||
let {tags} = await CommentsService.findById(id);
|
||||
expect(tags.length).to.equal(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('#remove', () => {
|
||||
it('removes a tag', async () => {
|
||||
const id = comment.id;
|
||||
const name = 'BEST';
|
||||
const assigned_by = user.id;
|
||||
|
||||
await TagsService.add(id, 'COMMENTS', {
|
||||
tag: {
|
||||
name
|
||||
},
|
||||
assigned_by
|
||||
});
|
||||
|
||||
{
|
||||
const {tags} = await CommentsService.findById(id);
|
||||
expect(tags.length).to.equal(1);
|
||||
}
|
||||
|
||||
// ok now to remove it
|
||||
await TagsService.remove(id, 'COMMENTS', {
|
||||
tag: {
|
||||
name
|
||||
},
|
||||
assigned_by
|
||||
});
|
||||
|
||||
{
|
||||
const {tags} = await CommentsService.findById(id);
|
||||
expect(tags.length).to.equal(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5060,6 +5060,10 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0:
|
||||
dependencies:
|
||||
js-tokens "^3.0.0"
|
||||
|
||||
lower-case@^1.1.1:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac"
|
||||
|
||||
lowercase-keys@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
|
||||
@@ -5445,6 +5449,12 @@ nightwatch@^0.9.11:
|
||||
proxy-agent "2.0.0"
|
||||
q "1.4.1"
|
||||
|
||||
no-case@^2.2.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.1.tgz#7aeba1c73a52184265554b7dc03baf720df80081"
|
||||
dependencies:
|
||||
lower-case "^1.1.1"
|
||||
|
||||
nocache@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.0.0.tgz#202b48021a0c4cbde2df80de15a17443c8b43980"
|
||||
@@ -7565,6 +7575,12 @@ smtp-connection@2.12.0:
|
||||
httpntlm "1.6.1"
|
||||
nodemailer-shared "1.1.0"
|
||||
|
||||
snake-case@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-2.1.0.tgz#41bdb1b73f30ec66a04d4e2cad1b76387d4d6d9f"
|
||||
dependencies:
|
||||
no-case "^2.2.0"
|
||||
|
||||
sntp@1.x.x:
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
|
||||
|
||||
Reference in New Issue
Block a user