adjusted index creation and managemen

This commit is contained in:
Wyatt Johnson
2018-04-12 09:37:17 -06:00
parent 68473e5a29
commit 10d11c1615
17 changed files with 1027 additions and 926 deletions
+17
View File
@@ -1,11 +1,26 @@
# job_environment will setup the environment for any job being executed.
job_environment: &job_environment
DISABLE_CREATE_MONGO_INDEXES: TRUE
# job_defaults applies all the defaults for each job.
job_defaults: &job_defaults
working_directory: ~/coralproject/talk
docker:
- image: circleci/node:8
environment:
<<: *job_environment
# create_indexes will create the mongo indexes and wait until they have been
# built.
create_indexes: &create_indexes
run:
name: Create the database indexes and wait until they are built
command: ./bin/cli db createIndexes
# integration_environment is the environment that configures the tests.
integration_environment: &integration_environment
<<: *job_environment
NODE_ENV: test
CIRCLE_TEST_REPORTS: /tmp/circleci-test-results
E2E_MAX_RETRIES: 3
@@ -25,6 +40,7 @@ integration_job: &integration_job
- checkout
- attach_workspace:
at: ~/coralproject/talk
- <<: *create_indexes
- run:
name: Setup the database with defaults
command: ./bin/cli setup --defaults
@@ -117,6 +133,7 @@ jobs:
environment:
JEST_JUNIT_OUTPUT: /tmp/circleci-test-results/jest/test-results.xml
JEST_REPORTER: jest-junit
- <<: *create_indexes
- run:
name: Run the server unit tests
command: yarn test:server
+1
View File
@@ -11,6 +11,7 @@ const Matcher = require('did-you-mean');
program
.command('serve', 'serve the application')
.command('db', 'run database commands')
.command('settings', 'interact with the application settings')
.command('assets', 'interact with assets')
.command('setup', 'setup the application')
Executable
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env node
const util = require('./util');
const program = require('commander');
const config = require('../config');
async function createIndexes() {
try {
// Ensure we enable the index creation.
config.CREATE_MONGO_INDEXES = true;
// TODO: handle the plugin index creation?
// Let's register the shutdown hooks.
util.onshutdown([() => require('../services/mongoose').disconnect()]);
// Lets create all the database indexes for the application and wait for all
// them to finish their indexing.
const models = [
require('../models/action'),
require('../models/asset'),
require('../models/comment'),
require('../models/setting'),
require('../models/user'),
require('../models/migration'),
];
// Call the `.init()` method to setup all the indexes on each model.
// `init()` returns a promise that resolves when the indexes have finished
// building successfully. The `init()` function is idempotent, so we don't
// have to worry about triggering an index rebuild.
await Promise.all(models.map(Model => Model.init()));
console.log('Indexes created');
util.shutdown(0);
} catch (err) {
console.error(err);
util.shutdown(1);
}
}
program
.command('createIndexes')
.description('creates the database indexes and waits until they are created')
.action(createIndexes);
program.parse(process.argv);
// If there is no command listed, output help.
if (process.argv.length <= 2) {
program.outputHelp();
util.shutdown();
}
+2 -51
View File
@@ -1,53 +1,4 @@
const mongoose = require('../services/mongoose');
const uuid = require('uuid');
const Schema = mongoose.Schema;
const ACTION_TYPES = require('./enum/action_types');
const ITEM_TYPES = require('./enum/item_types');
const { Action } = require('./schema');
const ActionSchema = new Schema(
{
id: {
type: String,
default: uuid.v4,
unique: true,
},
action_type: {
type: String,
enum: ACTION_TYPES,
},
item_type: {
type: String,
enum: ITEM_TYPES,
},
item_id: String,
user_id: String,
// The element that summaries will additionally group on in addtion to their action_type, item_type, and
// item_id.
group_id: String,
// Additional metadata stored on the field.
metadata: Schema.Types.Mixed,
},
{
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at',
},
}
);
// Create an index on the `item_id` field so that queries looking for
// actions based on the item id can resolve faster.
ActionSchema.index(
{
item_id: 1,
},
{
background: true,
}
);
const Action = mongoose.model('Action', ActionSchema);
module.exports = Action;
module.exports = mongoose.model('Action', Action);
+2 -97
View File
@@ -1,99 +1,4 @@
const mongoose = require('../services/mongoose');
const Schema = mongoose.Schema;
const uuid = require('uuid');
const TagLinkSchema = require('./schema/tag_link');
const get = require('lodash/get');
const { Asset } = require('./schema');
const AssetSchema = new Schema(
{
id: {
type: String,
default: uuid.v4,
unique: true,
index: true,
},
url: {
type: String,
unique: true,
index: true,
},
type: {
type: String,
default: 'assets',
},
scraped: {
type: Date,
default: null,
},
closedAt: {
type: Date,
default: null,
},
closedMessage: {
type: String,
default: null,
},
title: String,
description: String,
image: String,
section: String,
subsection: String,
author: String,
publication_date: Date,
modified_date: Date,
// This object is used exclusively for storing settings that are to override
// the base settings from the base Settings object. This is to be accessed
// always after running `rectifySettings` against it.
settings: {
default: {},
type: Object,
},
// Tags are added by the self or by administrators.
tags: [TagLinkSchema],
// Additional metadata stored on the field.
metadata: {
default: {},
type: Object,
},
},
{
versionKey: false,
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at',
},
}
);
AssetSchema.index(
{
title: 'text',
url: 'text',
description: 'text',
section: 'text',
subsection: 'text',
author: 'text',
},
{
background: true,
}
);
/**
* Returns true if the asset is closed, false else.
*/
AssetSchema.virtual('isClosed').get(function() {
const closedAt = get(this, 'closedAt', null);
if (closedAt === null) {
return false;
}
return closedAt.getTime() <= new Date().getTime();
});
const Asset = mongoose.model('Asset', AssetSchema);
module.exports = Asset;
module.exports = mongoose.model('Asset', Asset);
+2 -233
View File
@@ -1,235 +1,4 @@
const mongoose = require('../services/mongoose');
const Schema = mongoose.Schema;
const TagLinkSchema = require('./schema/tag_link');
const uuid = require('uuid');
const COMMENT_STATUS = require('./enum/comment_status');
const { Comment } = require('./schema');
/**
* The Mongo schema for a Comment Status.
* @type {Schema}
*/
const StatusSchema = new Schema(
{
type: {
type: String,
enum: COMMENT_STATUS,
},
// The User ID of the user that assigned the status.
assigned_by: {
type: String,
default: null,
},
created_at: Date,
},
{
_id: false,
}
);
/**
* A record of old body values for a Comment
*/
const BodyHistoryItemSchema = new Schema({
body: {
required: true,
type: String,
},
// datetime until the comment body value was this.body
created_at: {
required: true,
type: Date,
default: Date,
},
});
/**
* The Mongo schema for a Comment.
* @type {Schema}
*/
const CommentSchema = new Schema(
{
id: {
type: String,
default: uuid.v4,
unique: true,
},
body: {
type: String,
required: [true, 'The body is required.'],
minlength: 2,
},
body_history: [BodyHistoryItemSchema],
asset_id: String,
author_id: String,
status_history: [StatusSchema],
status: {
type: String,
enum: COMMENT_STATUS,
default: 'NONE',
},
// parent_id is the id of the parent comment (null if there is none).
parent_id: String,
// The number of replies to this comment directly.
reply_count: {
type: Number,
default: 0,
},
// Counts to store related to actions taken on the given comment.
action_counts: {
default: {},
type: Object,
},
// Tags are added by the self or by administrators.
tags: [TagLinkSchema],
// Additional metadata stored on the field.
metadata: {
default: {},
type: Object,
},
},
{
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at',
},
toJSON: {
virtuals: true,
},
}
);
// Add the indexes for the id of the comment.
CommentSchema.index(
{
id: 1,
},
{
unique: true,
background: false,
}
);
CommentSchema.index(
{
status: 1,
created_at: 1,
},
{
background: true,
}
);
CommentSchema.index(
{
status: 1,
created_at: 1,
asset_id: 1,
},
{
background: true,
}
);
// Create a sparse index to search across.
CommentSchema.index(
{
created_at: 1,
'action_counts.flag': 1,
status: 1,
},
{
background: true,
sparse: true,
}
);
// Create a sparse index to search across.
CommentSchema.index(
{
'action_counts.flag': 1,
status: 1,
},
{
background: true,
sparse: true,
}
);
// Add an index that is optimized for finding flagged comments.
CommentSchema.index(
{
asset_id: 1,
created_at: 1,
'action_counts.flag': 1,
},
{
background: true,
}
);
// Add an index for the reply sort.
CommentSchema.index(
{
asset_id: 1,
created_at: -1,
reply_count: -1,
},
{
background: true,
}
);
// Optimize for tag searches/counts.
CommentSchema.index(
{
asset_id: 1,
'tags.tag.name': 1,
status: 1,
},
{
background: true,
}
);
// Optimize for tag searches/counts.
CommentSchema.index(
{
'tags.tag.name': 1,
status: 1,
},
{
background: true,
sparse: true,
}
);
// Add an index that is optimized for sorting based on the created_at timestamp
// but also good at locating comments that have a specific asset id.
CommentSchema.index(
{
asset_id: 1,
created_at: 1,
},
{
background: true,
}
);
CommentSchema.virtual('edited').get(function() {
return this.body_history.length > 1;
});
// Visable is true when the comment is visible to the public.
CommentSchema.virtual('visible').get(function() {
return ['ACCEPTED', 'NONE'].includes(this.status);
});
module.exports = mongoose.model('Comment', CommentSchema);
module.exports = mongoose.model('Comment', Comment);
+2 -8
View File
@@ -1,10 +1,4 @@
const mongoose = require('../services/mongoose');
const Schema = mongoose.Schema;
const { Migration } = require('./schema');
const MigrationSchema = new Schema({
version: Number,
});
const Migration = mongoose.model('Migration', MigrationSchema);
module.exports = Migration;
module.exports = mongoose.model('Migration', Migration);
+51
View File
@@ -0,0 +1,51 @@
const mongoose = require('../../services/mongoose');
const uuid = require('uuid');
const Schema = mongoose.Schema;
const ACTION_TYPES = require('../enum/action_types');
const ITEM_TYPES = require('../enum/item_types');
const Action = new Schema(
{
id: {
type: String,
default: uuid.v4,
unique: true,
},
action_type: {
type: String,
enum: ACTION_TYPES,
},
item_type: {
type: String,
enum: ITEM_TYPES,
},
item_id: String,
user_id: String,
// The element that summaries will additionally group on in addtion to their action_type, item_type, and
// item_id.
group_id: String,
// Additional metadata stored on the field.
metadata: Schema.Types.Mixed,
},
{
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at',
},
}
);
// Create an index on the `item_id` field so that queries looking for
// actions based on the item id can resolve faster.
Action.index(
{
item_id: 1,
},
{
background: true,
}
);
module.exports = Action;
+97
View File
@@ -0,0 +1,97 @@
const mongoose = require('../../services/mongoose');
const Schema = mongoose.Schema;
const uuid = require('uuid');
const TagLinkSchema = require('./tag_link');
const { get } = require('lodash');
const Asset = new Schema(
{
id: {
type: String,
default: uuid.v4,
unique: true,
index: true,
},
url: {
type: String,
unique: true,
index: true,
},
type: {
type: String,
default: 'assets',
},
scraped: {
type: Date,
default: null,
},
closedAt: {
type: Date,
default: null,
},
closedMessage: {
type: String,
default: null,
},
title: String,
description: String,
image: String,
section: String,
subsection: String,
author: String,
publication_date: Date,
modified_date: Date,
// This object is used exclusively for storing settings that are to override
// the base settings from the base Settings object. This is to be accessed
// always after running `rectifySettings` against it.
settings: {
default: {},
type: Object,
},
// Tags are added by the self or by administrators.
tags: [TagLinkSchema],
// Additional metadata stored on the field.
metadata: {
default: {},
type: Object,
},
},
{
versionKey: false,
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at',
},
}
);
Asset.index(
{
title: 'text',
url: 'text',
description: 'text',
section: 'text',
subsection: 'text',
author: 'text',
},
{
background: true,
}
);
/**
* Returns true if the asset is closed, false else.
*/
Asset.virtual('isClosed').get(function() {
const closedAt = get(this, 'closedAt', null);
if (closedAt === null) {
return false;
}
return closedAt.getTime() <= new Date().getTime();
});
module.exports = Asset;
+235
View File
@@ -0,0 +1,235 @@
const mongoose = require('../../services/mongoose');
const Schema = mongoose.Schema;
const TagLinkSchema = require('./tag_link');
const uuid = require('uuid');
const COMMENT_STATUS = require('../enum/comment_status');
/**
* The Mongo schema for a Comment Status.
* @type {Schema}
*/
const Status = new Schema(
{
type: {
type: String,
enum: COMMENT_STATUS,
},
// The User ID of the user that assigned the status.
assigned_by: {
type: String,
default: null,
},
created_at: Date,
},
{
_id: false,
}
);
/**
* A record of old body values for a Comment
*/
const BodyHistoryItemSchema = new Schema({
body: {
required: true,
type: String,
},
// datetime until the comment body value was this.body
created_at: {
required: true,
type: Date,
default: Date,
},
});
/**
* The Mongo schema for a Comment.
* @type {Schema}
*/
const Comment = new Schema(
{
id: {
type: String,
default: uuid.v4,
unique: true,
},
body: {
type: String,
required: [true, 'The body is required.'],
minlength: 2,
},
body_history: [BodyHistoryItemSchema],
asset_id: String,
author_id: String,
status_history: [Status],
status: {
type: String,
enum: COMMENT_STATUS,
default: 'NONE',
},
// parent_id is the id of the parent comment (null if there is none).
parent_id: String,
// The number of replies to this comment directly.
reply_count: {
type: Number,
default: 0,
},
// Counts to store related to actions taken on the given comment.
action_counts: {
default: {},
type: Object,
},
// Tags are added by the self or by administrators.
tags: [TagLinkSchema],
// Additional metadata stored on the field.
metadata: {
default: {},
type: Object,
},
},
{
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at',
},
toJSON: {
virtuals: true,
},
}
);
// Add the indexes for the id of the comment.
Comment.index(
{
id: 1,
},
{
unique: true,
background: false,
}
);
Comment.index(
{
status: 1,
created_at: 1,
},
{
background: true,
}
);
Comment.index(
{
status: 1,
created_at: 1,
asset_id: 1,
},
{
background: true,
}
);
// Create a sparse index to search across.
Comment.index(
{
created_at: 1,
'action_counts.flag': 1,
status: 1,
},
{
background: true,
sparse: true,
}
);
// Create a sparse index to search across.
Comment.index(
{
'action_counts.flag': 1,
status: 1,
},
{
background: true,
sparse: true,
}
);
// Add an index that is optimized for finding flagged comments.
Comment.index(
{
asset_id: 1,
created_at: 1,
'action_counts.flag': 1,
},
{
background: true,
}
);
// Add an index for the reply sort.
Comment.index(
{
asset_id: 1,
created_at: -1,
reply_count: -1,
},
{
background: true,
}
);
// Optimize for tag searches/counts.
Comment.index(
{
asset_id: 1,
'tags.tag.name': 1,
status: 1,
},
{
background: true,
}
);
// Optimize for tag searches/counts.
Comment.index(
{
'tags.tag.name': 1,
status: 1,
},
{
background: true,
sparse: true,
}
);
// Add an index that is optimized for sorting based on the created_at timestamp
// but also good at locating comments that have a specific asset id.
Comment.index(
{
asset_id: 1,
created_at: 1,
},
{
background: true,
}
);
Comment.virtual('edited').get(function() {
return this.body_history.length > 1;
});
// Visible is true when the comment is visible to the public.
Comment.virtual('visible').get(function() {
return ['ACCEPTED', 'NONE'].includes(this.status);
});
module.exports = Comment;
+21
View File
@@ -0,0 +1,21 @@
const { CREATE_MONGO_INDEXES } = require('../../config');
const Action = require('./action');
const Asset = require('./asset');
const Comment = require('./comment');
const Migration = require('./migration');
const Setting = require('./setting');
const User = require('./user');
const schema = { Action, Asset, Comment, Migration, Setting, User };
// Provide the schema to each of the plugins so that they can add in indexes if
// it is enabled.
if (CREATE_MONGO_INDEXES) {
const plugins = require('../../services/plugins');
plugins.get('server', 'indexes').map(({ indexes }) => {
indexes(schema);
});
}
module.exports = schema;
+8
View File
@@ -0,0 +1,8 @@
const mongoose = require('../../services/mongoose');
const Schema = mongoose.Schema;
const Migration = new Schema({
version: Number,
});
module.exports = Migration;
+142
View File
@@ -0,0 +1,142 @@
const mongoose = require('../../services/mongoose');
const Schema = mongoose.Schema;
const TagSchema = require('./tag');
const MODERATION_OPTIONS = require('../enum/moderation_options');
/**
* Setting manages application settings that get used on front and backend.
* @type {Schema}
*/
const Setting = new Schema(
{
id: {
type: String,
default: '1',
},
moderation: {
type: String,
enum: MODERATION_OPTIONS,
default: 'POST',
},
infoBoxEnable: {
type: Boolean,
default: false,
},
customCssUrl: {
type: String,
default: '',
},
infoBoxContent: {
type: String,
default: '',
},
questionBoxEnable: {
type: Boolean,
default: false,
},
questionBoxIcon: {
type: String,
default: 'default',
},
questionBoxContent: {
type: String,
default: '',
},
premodLinksEnable: {
type: Boolean,
default: false,
},
organizationName: {
type: String,
},
autoCloseStream: {
type: Boolean,
default: false,
},
closedTimeout: {
type: Number,
// Two weeks default expiry.
default: 60 * 60 * 24 * 7 * 2,
},
closedMessage: {
type: String,
default: 'Expired',
},
wordlist: {
banned: {
type: Array,
default: [],
},
suspect: {
type: Array,
default: [],
},
},
charCount: {
type: Number,
default: 5000,
},
charCountEnable: {
type: Boolean,
default: false,
},
requireEmailConfirmation: {
type: Boolean,
default: false,
},
domains: {
whitelist: {
type: Array,
default: ['localhost'],
},
},
// Length of time (in milliseconds) after a comment is posted that it can still be edited by the author
editCommentWindowLength: {
type: Number,
min: [0, 'Edit Comment Window length must be greater than zero'],
default: 30 * 1000,
},
tags: [TagSchema],
// Additional metadata to let plugins write settings.
metadata: {
default: {},
type: Object,
},
},
{
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at',
},
toObject: {
transform: (doc, ret) => {
delete ret._id;
delete ret.__v;
return ret;
},
},
}
);
/**
* Merges two settings objects.
*/
Setting.method('merge', function(src) {
Setting.eachPath(path => {
// Exclude internal fields...
if (['id', '_id', '__v', 'created_at', 'updated_at'].includes(path)) {
return;
}
// If the source object contains the path, shallow copy it.
if (path in src) {
this[path] = src[path];
}
});
});
module.exports = Setting;
+375
View File
@@ -0,0 +1,375 @@
const mongoose = require('../../services/mongoose');
const bcrypt = require('bcryptjs');
const Schema = mongoose.Schema;
const uuid = require('uuid');
const TagLink = require('./tag_link');
const Token = require('./token');
const can = require('../../perms');
const { get } = require('lodash');
// USER_ROLES is the array of roles that is permissible as a user role.
const USER_ROLES = require('../enum/user_roles');
// USER_STATUS_USERNAME is the list of statuses that are supported by storing
// the username state.
const USER_STATUS_USERNAME = require('../enum/user_status_username');
// Profile is the mongoose schema defined as the representation of a
// User's profile stored in MongoDB.
const Profile = 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,
// the id would be the foreign providers identifier.
id: {
type: String,
required: true,
},
// Provider is simply the name attached to the authentication mode. In the
// case of a locally provided profile, this will simply be `local`, or a
// social provider which for Facebook would just be `facebook`.
provider: {
type: String,
required: true,
},
// Metadata provides a place to put provider specific details. An example of
// something that could be stored here is the `metadata.confirmed_at` could be
// used by the `local` provider to indicate when the email address was
// confirmed.
metadata: {
type: Schema.Types.Mixed,
},
},
{
_id: false,
}
);
// User is the mongoose schema defined as the representation of a User in
// MongoDB.
const User = new Schema(
{
// This ID represents the most unique identifier for a user, it is generated
// when the user is created as a random uuid.
id: {
type: String,
default: uuid.v4,
unique: true,
required: true,
},
// This is sourced from the social provider or set manually during user setup
// and simply provides a name to display for the given user.
username: {
type: String,
required: true,
},
// TODO: find a way that we can instead utilize MongoDB 3.4's collation
// options to build the index in a case insenstive manner:
// https://docs.mongodb.com/manual/reference/collation/
lowercaseUsername: {
type: String,
required: true,
unique: true,
},
// This provides a source of identity proof for users who login using the
// local provider. A local provider will be assumed for users who do not
// have any social profiles.
password: String,
// Profiles describes the array of identities for a given user. Any one user
// can have multiple profiles associated with them, including multiple email
// addresses.
profiles: [Profile],
// Tokens are the individual personal access tokens for a given user.
tokens: [Token],
// Role is the specific user role that the user holds.
role: {
type: String,
enum: USER_ROLES,
required: true,
default: 'COMMENTER',
},
// Status stores the user status information regarding permissions,
// capabilities and moderation state.
status: {
// Username stores the current user status for the username as well as the
// history of changes.
username: {
// Status stores the current username status.
status: {
type: String,
enum: USER_STATUS_USERNAME,
},
// History stores the history of username status changes.
history: [
{
// Status stores the historical username status.
status: {
type: String,
enum: USER_STATUS_USERNAME,
},
// assigned_by stores the user id of the user who assigned this status.
assigned_by: { type: String, default: null },
// created_at stores the date when this status was assigned.
created_at: { type: Date, default: Date.now },
},
],
},
// Banned stores the current user banned status as well as the history of
// changes.
banned: {
// Status stores the current user banned status.
status: {
type: Boolean,
required: true,
default: false,
},
history: [
{
// Status stores the historical banned status.
status: Boolean,
// assigned_by stores the user id of the user who assigned this status.
assigned_by: { type: String, default: null },
// message stores the email content sent to the user.
message: { type: String, default: null },
// created_at stores the date when this status was assigned.
created_at: { type: Date, default: Date.now },
},
],
},
// Suspension stores the current user suspension status as well as the
// history of changes.
suspension: {
// until is the date that the user is suspended until.
until: {
type: Date,
default: null,
},
history: [
{
// until is the date that the user is suspended until.
until: Date,
// assigned_by stores the user id of the user who assigned this status.
assigned_by: { type: String, default: null },
// message stores the email content sent to the user.
message: { type: String, default: null },
// created_at stores the date when this status was assigned.
created_at: { type: Date, default: Date.now },
},
],
},
},
// IgnoresUsers is an array of user id's that the current user is ignoring.
ignoresUsers: [String],
// Counts to store related to actions taken on the given user.
action_counts: {
default: {},
type: Object,
},
// Tags are added by the self or by administrators.
tags: [TagLink],
// Additional metadata stored on the field.
metadata: {
default: {},
type: Object,
},
},
{
// This will ensure that we have proper timestamps available on this model.
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at',
},
toJSON: {
transform: function(doc, ret) {
delete ret.__v;
delete ret._id;
delete ret.password;
},
},
}
);
// Add the index on the user profile data.
User.index(
{
'profiles.id': 1,
'profiles.provider': 1,
},
{
unique: true,
background: false,
}
);
User.index(
{
lowercaseUsername: 1,
'profiles.id': 1,
created_at: -1,
},
{
background: true,
}
);
// This query is executed often, to count the number of flagged accounts with
// usernames.
User.index(
{
'action_counts.flag': 1,
'status.username.status': 1,
},
{
background: true,
}
);
// Sorting users by created at is the default people search.
User.index(
{
created_at: -1,
},
{
background: true,
}
);
/**
* returns true if a commenter is staff
*/
User.method('isStaff', function() {
return this.role !== 'COMMENTER';
});
/**
* This verifies that a password is valid.
*/
User.method('verifyPassword', function(password) {
return new Promise((resolve, reject) => {
bcrypt.compare(password, this.password, (err, res) => {
if (err) {
return reject(err);
}
if (!res) {
return resolve(false);
}
return resolve(true);
});
});
});
/**
* Can returns true if the user is allowed to perform a specific graph
* operation.
*/
User.method('can', function(...actions) {
return can(this, ...actions);
});
/**
* firstEmail will return the first email on the user.
*/
User.virtual('firstEmail').get(function() {
const emails = this.emails;
if (emails.length === 0) {
return null;
}
return emails[0];
});
/**
* emails will return all the emails on a user.
*/
User.virtual('emails').get(function() {
return (this.profiles || [])
.filter(({ provider }) => provider === 'local')
.map(({ id }) => id);
});
/**
* hasVerifiedEmail will return true if at least one of the local email accounts
* have their email verified.
*/
User.virtual('hasVerifiedEmail').get(function() {
return this.profiles
.filter(({ provider }) => provider === 'local')
.some(profile => {
const confirmedAt = get(profile, 'metadata.confirmed_at') || null;
// If the profile doesn't have a metadata field, or it does not have a
// confirmed_at field, or that field is null, then send them back.
return confirmedAt !== null;
});
});
User.virtual('system')
.get(function() {
return this._system;
})
.set(function(system) {
this._system = system;
});
/**
* banned returns true when the user is currently banned, and sets the banned
* status locally.
*/
User.virtual('banned')
.get(function() {
return this.status.banned.status;
})
.set(function(status) {
this.status.banned.status = status;
this.status.banned.history.push({
status,
created_at: new Date(),
});
});
/**
* suspended returns true when the user is currently suspended, and sets the
* suspension status locally.
*/
User.virtual('suspended')
.get(function() {
return Boolean(
this.status.suspension.until && this.status.suspension.until > new Date()
);
})
.set(function(until) {
this.status.suspension.until = until;
this.status.suspension.history.push({
until,
created_at: new Date(),
});
});
module.exports = User;
+2 -145
View File
@@ -1,147 +1,4 @@
const mongoose = require('../services/mongoose');
const Schema = mongoose.Schema;
const TagSchema = require('./schema/tag');
const MODERATION_OPTIONS = require('./enum/moderation_options');
const { Setting } = require('./schema');
/**
* SettingSchema manages application settings that get used on front and backend.
* @type {Schema}
*/
const SettingSchema = new Schema(
{
id: {
type: String,
default: '1',
},
moderation: {
type: String,
enum: MODERATION_OPTIONS,
default: 'POST',
},
infoBoxEnable: {
type: Boolean,
default: false,
},
customCssUrl: {
type: String,
default: '',
},
infoBoxContent: {
type: String,
default: '',
},
questionBoxEnable: {
type: Boolean,
default: false,
},
questionBoxIcon: {
type: String,
default: 'default',
},
questionBoxContent: {
type: String,
default: '',
},
premodLinksEnable: {
type: Boolean,
default: false,
},
organizationName: {
type: String,
},
autoCloseStream: {
type: Boolean,
default: false,
},
closedTimeout: {
type: Number,
// Two weeks default expiry.
default: 60 * 60 * 24 * 7 * 2,
},
closedMessage: {
type: String,
default: 'Expired',
},
wordlist: {
banned: {
type: Array,
default: [],
},
suspect: {
type: Array,
default: [],
},
},
charCount: {
type: Number,
default: 5000,
},
charCountEnable: {
type: Boolean,
default: false,
},
requireEmailConfirmation: {
type: Boolean,
default: false,
},
domains: {
whitelist: {
type: Array,
default: ['localhost'],
},
},
// Length of time (in milliseconds) after a comment is posted that it can still be edited by the author
editCommentWindowLength: {
type: Number,
min: [0, 'Edit Comment Window length must be greater than zero'],
default: 30 * 1000,
},
tags: [TagSchema],
// Additional metadata to let plugins write settings.
metadata: {
default: {},
type: Object,
},
},
{
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at',
},
toObject: {
transform: (doc, ret) => {
delete ret._id;
delete ret.__v;
return ret;
},
},
}
);
/**
* Merges two settings objects.
*/
SettingSchema.method('merge', function(src) {
SettingSchema.eachPath(path => {
// Exclude internal fields...
if (['id', '_id', '__v', 'created_at', 'updated_at'].includes(path)) {
return;
}
// If the source object contains the path, shallow copy it.
if (path in src) {
this[path] = src[path];
}
});
});
/**
* The Mongo Mongoose object.
*/
const Setting = mongoose.model('Setting', SettingSchema);
module.exports = Setting;
module.exports = mongoose.model('Setting', Setting);
+2 -376
View File
@@ -1,378 +1,4 @@
const mongoose = require('../services/mongoose');
const bcrypt = require('bcryptjs');
const Schema = mongoose.Schema;
const uuid = require('uuid');
const TagLinkSchema = require('./schema/tag_link');
const TokenSchema = require('./schema/token');
const can = require('../perms');
const { get } = require('lodash');
const { User } = require('./schema');
// USER_ROLES is the array of roles that is permissible as a user role.
const USER_ROLES = require('./enum/user_roles');
// USER_STATUS_USERNAME is the list of statuses that are supported by storing
// the username state.
const USER_STATUS_USERNAME = require('./enum/user_status_username');
// ProfileSchema is the mongoose schema defined as the representation of a
// User's profile stored in MongoDB.
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,
// the id would be the foreign providers identifier.
id: {
type: String,
required: true,
},
// Provider is simply the name attached to the authentication mode. In the
// case of a locally provided profile, this will simply be `local`, or a
// social provider which for Facebook would just be `facebook`.
provider: {
type: String,
required: true,
},
// Metadata provides a place to put provider specific details. An example of
// something that could be stored here is the `metadata.confirmed_at` could be
// used by the `local` provider to indicate when the email address was
// confirmed.
metadata: {
type: Schema.Types.Mixed,
},
},
{
_id: false,
}
);
// UserSchema is the mongoose schema defined as the representation of a User in
// MongoDB.
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.
id: {
type: String,
default: uuid.v4,
unique: true,
required: true,
},
// This is sourced from the social provider or set manually during user setup
// and simply provides a name to display for the given user.
username: {
type: String,
required: true,
},
// TODO: find a way that we can instead utilize MongoDB 3.4's collation
// options to build the index in a case insenstive manner:
// https://docs.mongodb.com/manual/reference/collation/
lowercaseUsername: {
type: String,
required: true,
unique: true,
},
// This provides a source of identity proof for users who login using the
// local provider. A local provider will be assumed for users who do not
// have any social profiles.
password: String,
// Profiles describes the array of identities for a given user. Any one user
// can have multiple profiles associated with them, including multiple email
// addresses.
profiles: [ProfileSchema],
// Tokens are the individual personal access tokens for a given user.
tokens: [TokenSchema],
// Role is the specific user role that the user holds.
role: {
type: String,
enum: USER_ROLES,
required: true,
default: 'COMMENTER',
},
// Status stores the user status information regarding permissions,
// capabilities and moderation state.
status: {
// Username stores the current user status for the username as well as the
// history of changes.
username: {
// Status stores the current username status.
status: {
type: String,
enum: USER_STATUS_USERNAME,
},
// History stores the history of username status changes.
history: [
{
// Status stores the historical username status.
status: {
type: String,
enum: USER_STATUS_USERNAME,
},
// assigned_by stores the user id of the user who assigned this status.
assigned_by: { type: String, default: null },
// created_at stores the date when this status was assigned.
created_at: { type: Date, default: Date.now },
},
],
},
// Banned stores the current user banned status as well as the history of
// changes.
banned: {
// Status stores the current user banned status.
status: {
type: Boolean,
required: true,
default: false,
},
history: [
{
// Status stores the historical banned status.
status: Boolean,
// assigned_by stores the user id of the user who assigned this status.
assigned_by: { type: String, default: null },
// message stores the email content sent to the user.
message: { type: String, default: null },
// created_at stores the date when this status was assigned.
created_at: { type: Date, default: Date.now },
},
],
},
// Suspension stores the current user suspension status as well as the
// history of changes.
suspension: {
// until is the date that the user is suspended until.
until: {
type: Date,
default: null,
},
history: [
{
// until is the date that the user is suspended until.
until: Date,
// assigned_by stores the user id of the user who assigned this status.
assigned_by: { type: String, default: null },
// message stores the email content sent to the user.
message: { type: String, default: null },
// created_at stores the date when this status was assigned.
created_at: { type: Date, default: Date.now },
},
],
},
},
// IgnoresUsers is an array of user id's that the current user is ignoring.
ignoresUsers: [String],
// Counts to store related to actions taken on the given user.
action_counts: {
default: {},
type: Object,
},
// Tags are added by the self or by administrators.
tags: [TagLinkSchema],
// Additional metadata stored on the field.
metadata: {
default: {},
type: Object,
},
},
{
// This will ensure that we have proper timestamps available on this model.
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at',
},
toJSON: {
transform: function(doc, ret) {
delete ret.__v;
delete ret._id;
delete ret.password;
},
},
}
);
// Add the index on the user profile data.
UserSchema.index(
{
'profiles.id': 1,
'profiles.provider': 1,
},
{
unique: true,
background: false,
}
);
UserSchema.index(
{
lowercaseUsername: 1,
'profiles.id': 1,
created_at: -1,
},
{
background: true,
}
);
// This query is executed often, to count the number of flagged accounts with
// usernames.
UserSchema.index(
{
'action_counts.flag': 1,
'status.username.status': 1,
},
{
background: true,
}
);
// Sorting users by created at is the default people search.
UserSchema.index(
{
created_at: -1,
},
{
background: true,
}
);
/**
* returns true if a commenter is staff
*/
UserSchema.method('isStaff', function() {
return this.role !== 'COMMENTER';
});
/**
* This verifies that a password is valid.
*/
UserSchema.method('verifyPassword', function(password) {
return new Promise((resolve, reject) => {
bcrypt.compare(password, this.password, (err, res) => {
if (err) {
return reject(err);
}
if (!res) {
return resolve(false);
}
return resolve(true);
});
});
});
/**
* Can returns true if the user is allowed to perform a specific graph
* operation.
*/
UserSchema.method('can', function(...actions) {
return can(this, ...actions);
});
/**
* firstEmail will return the first email on the user.
*/
UserSchema.virtual('firstEmail').get(function() {
const emails = this.emails;
if (emails.length === 0) {
return null;
}
return emails[0];
});
/**
* emails will return all the emails on a user.
*/
UserSchema.virtual('emails').get(function() {
return (this.profiles || [])
.filter(({ provider }) => provider === 'local')
.map(({ id }) => id);
});
/**
* hasVerifiedEmail will return true if at least one of the local email accounts
* have their email verified.
*/
UserSchema.virtual('hasVerifiedEmail').get(function() {
return this.profiles
.filter(({ provider }) => provider === 'local')
.some(profile => {
const confirmedAt = get(profile, 'metadata.confirmed_at') || null;
// If the profile doesn't have a metadata field, or it does not have a
// confirmed_at field, or that field is null, then send them back.
return confirmedAt !== null;
});
});
UserSchema.virtual('system')
.get(function() {
return this._system;
})
.set(function(system) {
this._system = system;
});
/**
* banned returns true when the user is currently banned, and sets the banned
* status locally.
*/
UserSchema.virtual('banned')
.get(function() {
return this.status.banned.status;
})
.set(function(status) {
this.status.banned.status = status;
this.status.banned.history.push({
status,
created_at: new Date(),
});
});
/**
* suspended returns true when the user is currently suspended, and sets the
* suspension status locally.
*/
UserSchema.virtual('suspended')
.get(function() {
return Boolean(
this.status.suspension.until && this.status.suspension.until > new Date()
);
})
.set(function(until) {
this.status.suspension.until = until;
this.status.suspension.history.push({
until,
created_at: new Date(),
});
});
// Create the User model.
const UserModel = mongoose.model('User', UserSchema);
module.exports = UserModel;
module.exports = mongoose.model('User', User);
+15 -16
View File
@@ -2,24 +2,23 @@ const { SEARCH_OTHER_USERS } = require('../../../perms/constants');
const { ErrNotFound, ErrAlreadyExists } = require('../../../errors');
const pluralize = require('pluralize');
const sc = require('snake-case');
const CommentModel = require('../../../models/comment');
const { CREATE_MONGO_INDEXES } = require('../../../config');
// const { CREATE_MONGO_INDEXES } = require('../../../config');
function getReactionConfig(reaction) {
reaction = reaction.toLowerCase();
if (CREATE_MONGO_INDEXES) {
// Create the index on the comment model based on the reaction config.
CommentModel.collection.createIndex(
{
created_at: 1,
[`action_counts.${sc(reaction)}`]: 1,
},
{
background: true,
}
);
}
// if (CREATE_MONGO_INDEXES) {
// // Create the index on the comment model based on the reaction config.
// CommentModel.collection.createIndex(
// {
// created_at: 1,
// [`action_counts.${sc(reaction)}`]: 1,
// },
// {
// background: true,
// }
// );
// }
const reactionPlural = pluralize(reaction);
const Reaction = reaction.charAt(0).toUpperCase() + reaction.slice(1);
@@ -128,8 +127,8 @@ function getReactionConfig(reaction) {
return {
typeDefs,
schemas: ({ CommentSchema }) => {
CommentSchema.index(
indexes: ({ Comment }) => {
Comment.index(
{
created_at: 1,
[`action_counts.${sc(reaction)}`]: 1,