mirror of
https://github.com/wassname/talk.git
synced 2026-06-29 22:35:19 +08:00
First pass
This commit is contained in:
@@ -77,6 +77,8 @@ app.use(session(session_opts));
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
app.use('/api/v1/graph', require('./routes/api/graph'));
|
||||
|
||||
//==============================================================================
|
||||
// CSRF MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
@@ -54,12 +54,16 @@
|
||||
"commander": "^2.9.0",
|
||||
"connect-redis": "^3.1.0",
|
||||
"csurf": "^1.9.0",
|
||||
"dataloader": "^1.2.0",
|
||||
"debug": "^2.2.0",
|
||||
"dotenv": "^4.0.0",
|
||||
"ejs": "^2.5.2",
|
||||
"env-rewrite": "^1.0.2",
|
||||
"express": "^4.14.0",
|
||||
"express-session": "^1.14.2",
|
||||
"graphql": "^0.8.2",
|
||||
"graphql-server-express": "^0.5.0",
|
||||
"graphql-tools": "^0.9.0",
|
||||
"helmet": "^3.1.0",
|
||||
"jsonwebtoken": "^7.1.9",
|
||||
"kue": "^0.11.5",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
const express = require('express');
|
||||
const apollo = require('graphql-server-express');
|
||||
const tools = require('graphql-tools');
|
||||
const resolvers = require('./resolvers');
|
||||
const typeDefs = require('./typeDefs');
|
||||
const loaders = require('./loaders');
|
||||
const mutators = require('./mutators');
|
||||
|
||||
const schema = tools.makeExecutableSchema({typeDefs, resolvers});
|
||||
const router = express.Router();
|
||||
|
||||
router.use('/ql', apollo.graphqlExpress((req) => {
|
||||
|
||||
let context = {req};
|
||||
|
||||
context.loaders = loaders(context);
|
||||
context.mutators = mutators(context);
|
||||
|
||||
return {
|
||||
schema,
|
||||
context
|
||||
};
|
||||
}));
|
||||
router.use('/iql', apollo.graphiqlExpress({endpointURL: '/api/v1/graph/ql'}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,97 @@
|
||||
const DataLoader = require('dataloader');
|
||||
const _ = require('lodash');
|
||||
|
||||
const Comment = require('../../../models/comment');
|
||||
const User = require('../../../models/user');
|
||||
const Action = require('../../../models/action');
|
||||
const Asset = require('../../../models/asset');
|
||||
const Settings = require('../../../models/setting');
|
||||
|
||||
class SingletonResolver {
|
||||
constructor(resolver) {
|
||||
this._cache = null;
|
||||
this._resolver = resolver;
|
||||
}
|
||||
|
||||
load() {
|
||||
if (this._cache) {
|
||||
return this._cache;
|
||||
}
|
||||
|
||||
let promise = this._resolver(arguments).then((result) => {
|
||||
return result;
|
||||
});
|
||||
|
||||
// Set the promise on the cache.
|
||||
this._cache = promise;
|
||||
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
const arrayJoinBy = (ids, key) => (items) => {
|
||||
const itemsByKey = _.groupBy(items, key);
|
||||
return ids.map((id) => {
|
||||
if (id in itemsByKey) {
|
||||
return itemsByKey[id];
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
};
|
||||
|
||||
const singleJoinBy = (ids, key) => (items) => {
|
||||
const itemsByKey = _.groupBy(items, key);
|
||||
return ids.map((id) => {
|
||||
if (id in itemsByKey) {
|
||||
return itemsByKey[id][0];
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
};
|
||||
|
||||
const genAssetByID = (ids) => Asset.find({
|
||||
id: {
|
||||
$in: ids
|
||||
}
|
||||
}).then(singleJoinBy(ids, 'id'));
|
||||
|
||||
const genActionsByID = (ids, user = {}) => Action.getActionSummaries(ids, user.id).then(arrayJoinBy(ids, 'item_id'));
|
||||
|
||||
const genCommentsByAssetID = (ids) => Comment.find({
|
||||
asset_id: {
|
||||
$in: ids
|
||||
},
|
||||
parent_id: null,
|
||||
status: {
|
||||
$in: [null, 'accepted']
|
||||
}
|
||||
}).then(arrayJoinBy(ids, 'asset_id'));
|
||||
|
||||
const genCommentsByParentID = (ids) => Comment.find({
|
||||
parent_id: {
|
||||
$in: ids
|
||||
},
|
||||
status: {
|
||||
$in: [null, 'accepted']
|
||||
}
|
||||
}).then(arrayJoinBy(ids, 'parent_id'));
|
||||
|
||||
module.exports = (context) => ({
|
||||
Comments: {
|
||||
getByParentID: new DataLoader((ids) => genCommentsByParentID(ids)),
|
||||
getByAssetID: new DataLoader((ids) => genCommentsByAssetID(ids)),
|
||||
},
|
||||
Actions: {
|
||||
getByID: new DataLoader((ids) => genActionsByID(ids, context.req.user)),
|
||||
},
|
||||
Users: {
|
||||
getByID: new DataLoader((ids) => User.findByIdArray(ids))
|
||||
},
|
||||
Assets: {
|
||||
getByID: new DataLoader((ids) => genAssetByID(ids)),
|
||||
getAll: new SingletonResolver(() => Asset.find({}))
|
||||
},
|
||||
Settings: new SingletonResolver(() => Settings.retrieve())
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
const errors = require('../../../errors');
|
||||
|
||||
const Asset = require('../../../models/asset');
|
||||
const Comment = require('../../../models/comment');
|
||||
|
||||
const createComment = (context, {body, asset_id, parent_id}, wordlist = {}) => {
|
||||
|
||||
// Decide the status based on whether or not the current asset/settings
|
||||
// has pre-mod enabled or not. If the comment was rejected based on the
|
||||
// wordlist, then reject it, otherwise if the moderation setting is
|
||||
// premod, set it to `premod`.
|
||||
let status;
|
||||
|
||||
if (wordlist.banned) {
|
||||
status = Promise.resolve('rejected');
|
||||
} else {
|
||||
status = Asset
|
||||
.rectifySettings(Asset.findById(asset_id).then((asset) => {
|
||||
if (!asset) {
|
||||
return Promise.reject(errors.ErrNotFound);
|
||||
}
|
||||
|
||||
// Check to see if the asset has closed commenting...
|
||||
if (asset.isClosed) {
|
||||
|
||||
// They have, ensure that we send back an error.
|
||||
return Promise.reject(new errors.ErrAssetCommentingClosed(asset.closedMessage));
|
||||
}
|
||||
|
||||
return asset;
|
||||
}))
|
||||
|
||||
// Return `premod` if pre-moderation is enabled and an empty "new" status
|
||||
// in the event that it is not in pre-moderation mode.
|
||||
.then(({moderation, charCountEnable, charCount}) => {
|
||||
|
||||
// Reject if the comment is too long
|
||||
if (charCountEnable && body.length > charCount) {
|
||||
return 'rejected';
|
||||
}
|
||||
return moderation === 'pre' ? 'premod' : null;
|
||||
});
|
||||
}
|
||||
|
||||
return status.then((status) => Comment.publicCreate({
|
||||
body,
|
||||
asset_id,
|
||||
parent_id,
|
||||
status,
|
||||
author_id: context.req.user.id
|
||||
}))
|
||||
.then((comment) => {
|
||||
if (wordlist.suspect) {
|
||||
return Comment
|
||||
.addAction(comment.id, null, 'flag', {field: 'body', details: 'Matched suspect word filters.'})
|
||||
.then(() => comment);
|
||||
}
|
||||
|
||||
return comment;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = (context) => ({
|
||||
createComment: (comment) => createComment(context, comment)
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
module.exports = {
|
||||
Query: {
|
||||
assets(_, args, {loaders}) {
|
||||
return loaders.Assets.getAll.load();
|
||||
},
|
||||
asset(_, {id}, {loaders}) {
|
||||
return loaders.Assets.getByID.load(id);
|
||||
},
|
||||
settings(_, args, {loaders}) {
|
||||
return loaders.Settings.load();
|
||||
}
|
||||
},
|
||||
Mutation: {
|
||||
createComment(_, {asset_id, parent_id, body}, {mutators}) {
|
||||
return mutators.createComment({asset_id, parent_id, body});
|
||||
}
|
||||
},
|
||||
Asset: {
|
||||
comments({id}, _, {loaders}) {
|
||||
return loaders.Comments.getByAssetID.load(id);
|
||||
},
|
||||
settings({settings = null}, _, {loaders}) {
|
||||
return loaders.Settings.load()
|
||||
.then((globalSettings) => {
|
||||
|
||||
if (settings) {
|
||||
settings = Object.assign({}, settings, globalSettings);
|
||||
} else {
|
||||
settings = globalSettings;
|
||||
}
|
||||
|
||||
return settings;
|
||||
});
|
||||
}
|
||||
},
|
||||
User: {
|
||||
actions({id}, _, {loaders}) {
|
||||
return loaders.Actions.getByID.load(id);
|
||||
}
|
||||
},
|
||||
Comment: {
|
||||
user({author_id}, _, {loaders}) {
|
||||
return loaders.Users.getByID.load(author_id);
|
||||
},
|
||||
replies({id}, _, {loaders}) {
|
||||
return loaders.Comments.getByParentID.load(id);
|
||||
},
|
||||
actions({id}, _, {loaders}) {
|
||||
return loaders.Actions.getByID.load(id);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
const typeDefs = [`
|
||||
type UserSettings {
|
||||
bio: String
|
||||
}
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
displayName: String!
|
||||
actions: [Action]
|
||||
settings: UserSettings
|
||||
}
|
||||
|
||||
type Comment {
|
||||
id: ID!
|
||||
body: String!
|
||||
user: User
|
||||
replies(limit: Int = 3): [Comment]
|
||||
actions: [Action]
|
||||
}
|
||||
|
||||
type Action {
|
||||
id: ID!
|
||||
item_id: ID!
|
||||
action_type: String!
|
||||
count: Int
|
||||
current_user: Action
|
||||
updated_at: String
|
||||
created_at: String
|
||||
}
|
||||
|
||||
type Settings {
|
||||
moderation: String
|
||||
infoBoxEnable: Boolean
|
||||
infoBoxContent: String
|
||||
closeTimeout: Int
|
||||
closedMessage: String
|
||||
charCountEnable: Boolean
|
||||
charCount: Int
|
||||
requireEmailConfirmation: Boolean
|
||||
}
|
||||
|
||||
type Asset {
|
||||
id: ID!
|
||||
title: String
|
||||
url: String
|
||||
comments: [Comment]
|
||||
settings: Settings!
|
||||
}
|
||||
|
||||
type Query {
|
||||
settings: Settings
|
||||
assets: [Asset]
|
||||
asset(id: ID!): Asset!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createComment(asset_id: ID!, parent_id: ID, body: String!): Comment
|
||||
}
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
`];
|
||||
|
||||
module.exports = typeDefs;
|
||||
Reference in New Issue
Block a user