Merge branch 'master' into profanity-verbiage

This commit is contained in:
David Erwin
2017-04-26 17:21:34 -04:00
committed by GitHub
21 changed files with 9911 additions and 1076 deletions
+32
View File
@@ -145,6 +145,10 @@ type RootMutation {
type RootQuery {
people: [Person!]
}
type Subscription {
leader: Person
}
```
Thanks to [gql-merge](https://www.npmjs.com/package/gql-merge) the contents of
@@ -259,6 +263,23 @@ If your post function accepts four parameters, then it can modify the field
result. It is *required* that the function resolves a promise (or returns) with
the modified value or simply the original if you didn't modify it.
#### Field: `setupFunctions`
```js
setupFunctions: {
leader: (options, args) => ({
leader: {
filter: (person) => person.place === 1
},
}),
}
```
Setup functions allow you to create filters that control which pubsub.publish() events
send data to the client. If the type in question contains args, clients may subscribe using those arguments to further filter their subscription.
For more information, see the [Apollo Docs](https://github.com/apollographql/graphql-subscriptions).
#### Field: `router`
```js
@@ -375,6 +396,10 @@ module.exports = {
type RootQuery {
people: [Person!]
}
type Subscription {
leader: Person
}
`,
context: {
Slack: () => ({
@@ -430,6 +455,13 @@ module.exports = {
}
}
}
},
setupFunctions: {
leader: (options, args) => ({
leader: {
filter: (person) => person.place === 1
}
}
}
};
+4 -33
View File
@@ -5,13 +5,11 @@ const path = require('path');
const helmet = require('helmet');
const {passport} = require('./services/passport');
const plugins = require('./services/plugins');
const session = require('express-session');
const enabled = require('debug').enabled;
const RedisStore = require('connect-redis')(session);
const redis = require('./services/redis');
const csrf = require('csurf');
const errors = require('./errors');
const graph = require('./graph');
const session = require('./services/session');
const {createGraphOptions} = require('./graph');
const apollo = require('graphql-server-express');
const app = express();
@@ -43,34 +41,7 @@ app.set('view engine', 'ejs');
// SESSION MIDDLEWARE
//==============================================================================
const session_opts = {
secret: process.env.TALK_SESSION_SECRET,
httpOnly: true,
rolling: true,
saveUninitialized: true,
resave: true,
unset: 'destroy',
name: 'talk.sid',
cookie: {
secure: false,
maxAge: 8.64e+7, // 24 hours for session token expiry
},
store: new RedisStore({
client: redis.createClient(),
})
};
if (app.get('env') === 'production') {
// Enable the secure cookie when we are in production mode.
session_opts.cookie.secure = true;
} else if (app.get('env') === 'test') {
// Add in the secret during tests.
session_opts.secret = 'keyboard cat';
}
app.use(session(session_opts));
app.use(session);
//==============================================================================
// PASSPORT MIDDLEWARE
@@ -96,7 +67,7 @@ app.use(passport.session());
//==============================================================================
// GraphQL endpoint.
app.use('/api/v1/graph/ql', apollo.graphqlExpress(graph.createGraphOptions));
app.use('/api/v1/graph/ql', apollo.graphqlExpress(createGraphOptions));
// Only include the graphiql tool if we aren't in production mode.
if (app.get('env') !== 'production') {
+20 -9
View File
@@ -1,13 +1,14 @@
#!/usr/bin/env node
const app = require('../app');
const program = require('./commander');
const http = require('http');
const app = require('../app');
const {createServer} = require('http');
const scraper = require('../services/scraper');
const mailer = require('../services/mailer');
const kue = require('../services/kue');
const mongoose = require('../services/mongoose');
const util = require('./util');
const {createSubscriptionManager} = require('../graph/subscriptions');
/**
* Get port from environment and store in Express.
@@ -20,7 +21,7 @@ app.set('port', port);
/**
* Create HTTP server.
*/
const server = http.createServer(app);
const server = createServer(app);
/**
* Event listener for HTTP server "error" event.
@@ -76,20 +77,29 @@ function normalizePort(val) {
function onListening() {
let addr = server.address();
let bind = typeof addr === 'string'
? `pipe ${ addr}`
: `port ${ addr.port}`;
console.log(`Listening on ${ bind}`);
? `pipe ${addr}`
: `port ${addr.port}`;
console.log(`API Server Listening on ${bind}`);
}
/**
* Start the app.
*/
function startApp() {
function startApp(program) {
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.listen(port, () => {
// Mount the websocket server if requested.
if (program.websockets) {
console.log(`Websocket Server Listening on ${port}`);
// Mount the subscriptions server on the application server.
createSubscriptionManager(server);
}
});
server.on('error', onError);
server.on('listening', onListening);
}
@@ -100,10 +110,11 @@ function startApp() {
program
.option('-j, --jobs', 'enable job processing on this thread')
.option('-w, --websockets', 'enable the websocket (subscriptions) handler on this thread')
.parse(process.argv);
// Start the application serving.
startApp();
startApp(program);
// Enable job processing on the thread if enabled.
if (program.jobs) {
@@ -31,7 +31,7 @@ const Comment = ({actions = [], comment, ...props}) => {
}
return (
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--8dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem}`}>
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem} ${props.selected ? styles.selected : ''}`}>
<div className={styles.container}>
<div className={styles.itemHeader}>
<div className={styles.author}>
+13 -1
View File
@@ -1,6 +1,18 @@
import ApolloClient, {addTypename} from 'apollo-client';
import getNetworkInterface from './transport';
// import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws';
// TODO: replace absolute reference with something loaded from the store/page.
// const wsClient = new SubscriptionClient('ws://localhost:3000/api/v1/live', {
// reconnect: true
// });
// const networkInterface = addGraphQLSubscriptions(
// getNetworkInterface(),
// wsClient,
// );
const networkInterface = getNetworkInterface();
export const client = new ApolloClient({
connectToDevTools: true,
queryTransformer: addTypename,
@@ -10,7 +22,7 @@ export const client = new ApolloClient({
}
return null;
},
networkInterface: getNetworkInterface()
networkInterface
});
export default client;
@@ -3,7 +3,7 @@
"post": "Post",
"cancel": "Cancel",
"reply": "Reply",
"comment": "Post a Comment",
"comment": "Post a comment",
"name": "Name",
"comment-post-notif": "Your comment has been posted.",
"comment-post-notif-premod": "Thank you for posting. Our moderation team will review your comment shortly.",
@@ -14,7 +14,7 @@
"post": "Publicar",
"cancel": "Cancelar",
"reply": "Responder",
"comment": "Publicar un Comentario",
"comment": "Publicar un comentario",
"name": "Nombre",
"comment-post-notif": "Tu comentario ha sido publicado.",
"comment-post-notif-premod": "Gracias por el comentario. Nuestro equipo de moderación va a revisarlo muy pronto.",
+8 -1
View File
@@ -1,5 +1,6 @@
const loaders = require('./loaders');
const mutators = require('./mutators');
const uuid = require('uuid');
const plugins = require('../services/plugins');
const debug = require('debug')('talk:graph:context');
@@ -31,7 +32,10 @@ const decorateContextPlugins = (context, contextPlugins) => contextPlugins.reduc
* Stores the request context.
*/
class Context {
constructor({user = null}) {
constructor({user = null}, pubsub) {
// Generate a new context id for the request.
this.id = uuid.v4();
// Load the current logged in user to `user`, otherwise this'll be null.
if (user) {
@@ -46,6 +50,9 @@ class Context {
// Decorate the plugin context.
this.plugins = decorateContextPlugins(this, contextPlugins);
// Bind the publish/subscribe to the context.
this.pubsub = pubsub;
}
}
+5 -2
View File
@@ -1,5 +1,7 @@
const schema = require('./schema');
const Context = require('./context');
const pubsub = require('./pubsub');
const {createSubscriptionManager} = require('./subscriptions');
module.exports = {
createGraphOptions: (req) => ({
@@ -9,6 +11,7 @@ module.exports = {
// Load in the new context here, this'll create the loaders + mutators for
// the lifespan of this request.
context: new Context(req)
})
context: new Context(req, pubsub)
}),
createSubscriptionManager
};
+7 -1
View File
@@ -16,7 +16,7 @@ const Wordlist = require('../../services/wordlist');
* @param {String} [status='NONE'] the status of the new comment
* @return {Promise} resolves to the created comment
*/
const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id = null, tags = []}, status = 'NONE') => {
const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, parent_id = null, tags = []}, status = 'NONE') => {
// Building array of tags
tags = tags.map(tag => ({name: tag}));
@@ -47,6 +47,12 @@ const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id =
Comments.parentCountByAssetID.incr(asset_id);
}
Comments.countByAssetID.incr(asset_id);
if (pubsub) {
// Publish the newly added comment via the subscription.
pubsub.publish('commentAdded', comment);
}
}
return comment;
+5
View File
@@ -0,0 +1,5 @@
const {RedisPubSub} = require('graphql-redis-subscriptions');
const {connectionOptions} = require('../services/redis');
module.exports = new RedisPubSub(connectionOptions);
+4
View File
@@ -5,6 +5,10 @@ module.exports = new GraphQLScalarType({
name: 'Date',
description: 'Date represented as an ISO8601 string',
serialize(value) {
if (typeof value === 'string') {
return value;
}
return value.toISOString();
},
parseValue(value) {
+2
View File
@@ -16,6 +16,7 @@ const LikeAction = require('./like_action');
const RootMutation = require('./root_mutation');
const RootQuery = require('./root_query');
const Settings = require('./settings');
const Subscription = require('./subscription');
const UserError = require('./user_error');
const User = require('./user');
const ValidationUserError = require('./validation_user_error');
@@ -39,6 +40,7 @@ let resolvers = {
RootMutation,
RootQuery,
Settings,
Subscription,
UserError,
User,
ValidationUserError,
+7
View File
@@ -0,0 +1,7 @@
const Subscription = {
commentAdded(comment) {
return comment;
}
};
module.exports = Subscription;
+60
View File
@@ -0,0 +1,60 @@
const {SubscriptionManager} = require('graphql-subscriptions');
const {SubscriptionServer} = require('subscriptions-transport-ws');
const _ = require('lodash');
const pubsub = require('./pubsub');
const schema = require('./schema');
const Context = require('./context');
const plugins = require('../services/plugins');
const {deserializeUser} = require('../services/subscriptions');
// Core setup functions
let setupFunctions = {
commentAdded: (options, args) => ({
commentAdded: {
filter: (comment) => comment.asset_id === args.asset_id
},
}),
};
/**
* Plugin support requires that we merge in existing setupFunctions with our new
* plugin based ones. This allows plugins to extend existing setupFunctions as well
* as provide new ones.
*/
setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {setupFunctions}) => {
return _.merge(acc, setupFunctions);
}, setupFunctions);
/**
* This creates a new subscription manager.
*/
const createSubscriptionManager = (server) => new SubscriptionServer({
subscriptionManager: new SubscriptionManager({
schema,
pubsub,
setupFunctions,
}),
onSubscribe: (parsedMessage, baseParams, connection) => {
// Attach the context per request.
baseParams.context = () => deserializeUser(connection.upgradeReq)
.then((req) => new Context(req, pubsub))
.catch((err) => {
console.error(err);
return new Context({}, pubsub);
});
return baseParams;
}
}, {
server,
path: '/api/v1/live'
});
module.exports = {
createSubscriptionManager
};
+9
View File
@@ -820,6 +820,14 @@ type RootMutation {
stopIgnoringUser(id: ID!): StopIgnoringUserResponse
}
################################################################################
## Subscriptions
################################################################################
type Subscription {
commentAdded(asset_id: ID!): Comment
}
################################################################################
## Schema
################################################################################
@@ -827,4 +835,5 @@ type RootMutation {
schema {
query: RootQuery
mutation: RootMutation
subscription: Subscription
}
+7 -1
View File
@@ -123,7 +123,13 @@ const UserSchema = new mongoose.Schema({
// user id of another user
type: String,
}]
}],
// Additional metadata stored on the field.
metadata: {
default: {},
type: Object
}
}, {
// This will ensure that we have proper timestamps available on this model.
+12 -9
View File
@@ -5,8 +5,8 @@
"main": "app.js",
"scripts": {
"postinstall": "./bin/cli plugins reconcile --skip-remote",
"start": "./bin/cli serve --jobs",
"dev-start": "nodemon --config .nodemon.json --exec \"./bin/cli -c .env serve --jobs\"",
"start": "./bin/cli serve -j -w",
"dev-start": "nodemon -w . -w bin/cli -w bin/cli-serve --config .nodemon.json --exec \"./bin/cli -c .env serve -j -w\"",
"build": "NODE_ENV=production webpack -p --config webpack.config.js --bail",
"build-watch": "NODE_ENV=development webpack --progress --config webpack.config.js --watch",
"lint": "eslint bin/* .",
@@ -68,10 +68,12 @@
"express-session": "^1.15.1",
"form-data": "^2.1.2",
"gql-merge": "^0.0.4",
"graphql": "^0.8.2",
"graphql": "^0.9.1",
"graphql-errors": "^2.1.0",
"graphql-server-express": "^0.5.0",
"graphql-tools": "^0.9.0",
"graphql-redis-subscriptions": "^1.1.5",
"graphql-server-express": "^0.6.0",
"graphql-subscriptions": "^0.3.1",
"graphql-tools": "^0.10.1",
"helmet": "^3.5.0",
"inquirer": "^3.0.6",
"joi": "^10.4.1",
@@ -84,7 +86,7 @@
"minimist": "^1.2.0",
"mongoose": "^4.9.1",
"morgan": "^1.8.1",
"natural": "^0.4.0",
"natural": "^0.5.0",
"node-emoji": "^1.5.1",
"node-fetch": "^1.6.3",
"nodemailer": "^2.6.4",
@@ -95,10 +97,10 @@
"react-apollo": "^1.1.0",
"react-recaptcha": "^2.2.6",
"redis": "^2.7.1",
"resolve": "^1.3.2",
"semver": "^5.3.0",
"uuid": "^3.0.1",
"simplemde": "^1.11.2",
"uuid": "^2.0.3"
"resolve": "^1.3.2",
"semver": "^5.3.0"
},
"devDependencies": {
"apollo-client": "^1.0.4",
@@ -177,6 +179,7 @@
"redux-thunk": "^2.1.0",
"regenerator": "^0.8.46",
"selenium-standalone": "^5.11.2",
"subscriptions-transport-ws": "^0.5.5-alpha.0",
"style-loader": "^0.16.0",
"supertest": "^2.0.1",
"timeago.js": "^2.0.3",
+27 -23
View File
@@ -2,31 +2,35 @@ const redis = require('redis');
const debug = require('debug')('talk:redis');
const url = process.env.TALK_REDIS_URL || 'redis://localhost';
const connectionOptions = {
url,
retry_strategy: function(options) {
if (options.error && options.error.code === 'ECONNREFUSED') {
// End reconnecting on a specific error and flush all commands with a individual error
return new Error('The server refused the connection');
}
if (options.total_retry_time > 1000 * 60 * 60) {
// End reconnecting after a specific timeout and flush all commands with a individual error
return new Error('Retry time exhausted');
}
if (options.times_connected > 10) {
// End reconnecting with built in error
return undefined;
}
// reconnect after
return Math.max(options.attempt * 100, 3000);
}
};
module.exports = {
connectionOptions,
createClient() {
let client = redis.createClient(url, {
retry_strategy: function(options) {
if (options.error && options.error.code === 'ECONNREFUSED') {
// End reconnecting on a specific error and flush all commands with a individual error
return new Error('The server refused the connection');
}
if (options.total_retry_time > 1000 * 60 * 60) {
// End reconnecting after a specific timeout and flush all commands with a individual error
return new Error('Retry time exhausted');
}
if (options.times_connected > 10) {
// End reconnecting with built in error
return undefined;
}
// reconnect after
return Math.max(options.attempt * 100, 3000);
}
});
let client = redis.createClient(connectionOptions);
client.ping((err) => {
if (err) {
+36
View File
@@ -0,0 +1,36 @@
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('./redis');
//==============================================================================
// SESSION MIDDLEWARE
//==============================================================================
const session_opts = {
secret: process.env.TALK_SESSION_SECRET,
httpOnly: true,
rolling: true,
saveUninitialized: true,
resave: true,
unset: 'destroy',
name: 'talk.sid',
cookie: {
secure: false,
maxAge: 8.64e+7, // 24 hours for session token expiry
},
store: new RedisStore({
client: redis.createClient(),
})
};
if (process.env.NODE_ENV === 'production') {
// Enable the secure cookie when we are in production mode.
session_opts.cookie.secure = true;
} else if (process.env.NODE_ENV === 'test') {
// Add in the secret during tests.
session_opts.secret = 'keyboard cat';
}
module.exports = session(session_opts);
+36
View File
@@ -0,0 +1,36 @@
const session = require('./session');
const passport = require('./passport');
// Session data does not automatically attach to websocket req objects.
// This middleware code looks for a user in the session and, if it exists,
// attaches it to the graph req.
const deserializeUser = (req) => {
return new Promise((resolve, reject) => {
session(req, {}, () => {
if ('session' in req && 'passport' in req.session && 'user' in req.session.passport) {
passport.deserializeUser(req.session.passport.user, (err, user) => {
if (err) {
return reject(err);
}
req.user = user;
return resolve(req);
});
}
// Remove the user from the request (if there was one)
if (req.user) {
delete req.user;
}
// Resolve with the request (user removed possibly).
return resolve(req);
});
});
};
module.exports = {
deserializeUser
};
+9614 -993
View File
File diff suppressed because it is too large Load Diff