- {lang.t('report')}
+ {lang.t('report')}flag
diff --git a/client/coral-sign-in/translations.json b/client/coral-sign-in/translations.json
index 824888cca..7ae84b2ff 100644
--- a/client/coral-sign-in/translations.json
+++ b/client/coral-sign-in/translations.json
@@ -25,14 +25,14 @@
"emailInUse": "Email address already in use",
"emailORusernameInUse": "Email address or Username already in use",
"requiredField": "This field is required",
- "passwordsDontMatch": "Passwords don\"t match.",
+ "passwordsDontMatch": "Passwords don't match.",
"specialCharacters": "Usernames can contain letters, numbers and _ only",
"checkTheForm": "Invalid Form. Please, check the fields"
},
"createdisplay": {
"writeyourusername": "Edit your username",
"yourusername": "Your username appears on every comment you post.",
- "ifyoudontchangeyourname": "If you don\"t change your username at this step, your Facebook display name will appear alongside of all your comments.",
+ "ifyoudontchangeyourname": "If you don't change your username at this step, your Facebook display name will appear alongside of all your comments.",
"username": "Username",
"continue": "Continue with the same Facebook username",
"save": "Save",
diff --git a/graph/hooks.js b/graph/hooks.js
index 499eb60b1..c5b7541b3 100644
--- a/graph/hooks.js
+++ b/graph/hooks.js
@@ -3,6 +3,7 @@ const {
GraphQLInterfaceType
} = require('graphql');
const debug = require('debug')('talk:graph:schema');
+const Joi = require('joi');
/**
* XXX taken from graphql-js: src/execution/execute.js, because that function
@@ -82,6 +83,8 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa
Object.keys(hooks).forEach((hook) => {
switch (hook) {
case 'pre':
+ Joi.assert(hooks.pre, Joi.func().maxArity(4));
+
debug(`adding pre hook to resolver ${typeName}.${fieldName} from plugin '${plugin.name}'`);
if (typeof hooks.pre !== 'function') {
@@ -91,6 +94,8 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa
acc.pre.push(hooks.pre);
break;
case 'post':
+ Joi.assert(hooks.pre, Joi.func().maxArity(5));
+
debug(`adding post hook to resolver ${typeName}.${fieldName} from plugin '${plugin.name}'`);
if (typeof hooks.post !== 'function') {
@@ -129,6 +134,9 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa
return;
}
+ // Ensure it matches the format we expect.
+ Joi.assert(post, Joi.array().items(Joi.func().maxArity(3)), `invalid post hooks were found for ${typeName}.${fieldName}`);
+
// Cache the original resolverType function.
let resolveType = field.resolveType;
diff --git a/graph/resolvers/asset.js b/graph/resolvers/asset.js
index 96bc8c1bc..c260d0070 100644
--- a/graph/resolvers/asset.js
+++ b/graph/resolvers/asset.js
@@ -1,4 +1,11 @@
const Asset = {
+ lastComment({id}, _, {loaders: {Comments}}) {
+ return Comments.getByQuery({
+ asset_id: id,
+ limit: 1,
+ parent_id: null
+ }).then(data => data[0]);
+ },
recentComments({id}, _, {loaders: {Comments}}) {
return Comments.genRecentComments.load(id);
},
diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql
index aeae1537c..0aac36aaa 100644
--- a/graph/typeDefs.graphql
+++ b/graph/typeDefs.graphql
@@ -416,6 +416,9 @@ type Asset {
# The URL that the asset is located on.
url: String
+ # Returns last comment
+ lastComment: Comment
+
# Returns recent comments
recentComments: [Comment]
diff --git a/models/asset.js b/models/asset.js
index f0046aef0..51ff42705 100644
--- a/models/asset.js
+++ b/models/asset.js
@@ -46,6 +46,12 @@ const AssetSchema = new Schema({
type: Schema.Types.Mixed,
default: null
},
+
+ // Additional metadata stored on the field.
+ metadata: {
+ default: {},
+ type: Object
+ }
}, {
versionKey: false,
timestamps: {
diff --git a/models/comment.js b/models/comment.js
index e6523e45f..0984a0832 100644
--- a/models/comment.js
+++ b/models/comment.js
@@ -75,7 +75,13 @@ const CommentSchema = new Schema({
default: 'NONE'
},
tags: [TagSchema],
- parent_id: String
+ parent_id: String,
+
+ // Additional metadata stored on the field.
+ metadata: {
+ default: {},
+ type: Object
+ }
}, {
timestamps: {
createdAt: 'created_at',
diff --git a/package.json b/package.json
index f4e2eb725..41d8732de 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "talk",
- "version": "1.4.0",
- "description": "A commenting platform from The Coral Project. https://coralproject.net",
+ "version": "1.5.0",
+ "description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
"main": "app.js",
"scripts": {
"postinstall": "./bin/cli plugins reconcile --skip-remote",
@@ -17,7 +17,7 @@
"e2e": "NODE_ENV=test nightwatch",
"poste2e": "NODE_ENV=test scripts/poste2e.sh",
"embed-start": "NODE_ENV=development yarn build && ./bin/cli serve --jobs",
- "heroku-postbuild": "yarn build"
+ "heroku-postbuild": "./bin/cli plugins reconcile && yarn build"
},
"config": {
"pre-git": {
diff --git a/plugins.js b/plugins.js
index 02cdb58e1..22293c975 100644
--- a/plugins.js
+++ b/plugins.js
@@ -47,8 +47,8 @@ const hookSchemas = {
pre: Joi.func(),
post: Joi.func()
}))),
- loaders: Joi.object().pattern(/\w/, Joi.object().pattern(/\w/, Joi.func())),
- mutators: Joi.object().pattern(/\w/, Joi.object().pattern(/\w/, Joi.func())),
+ loaders: Joi.func().maxArity(1),
+ mutators: Joi.func().maxArity(1),
resolvers: Joi.object().pattern(/\w/, Joi.object().pattern(/(?:__resolveType|\w+)/, Joi.func())),
typeDefs: Joi.string()
};
@@ -124,7 +124,12 @@ function itteratePlugins(plugins) {
// Add each plugin folder to the allowed import path so that they can import our
// internal dependancies.
Object.keys(plugins).forEach((type) => itteratePlugins(plugins[type]).forEach((plugin) => {
- amp.enableForDir(path.dirname(plugin.path));
+
+ // The plugin may be remote, and therefore not installed. We check here if the
+ // plugin path is available before trying to monkeypatch it's require path.
+ if (plugin.path) {
+ amp.enableForDir(path.dirname(plugin.path));
+ }
}));
/**
diff --git a/services/email/suspension.html.ejs b/services/email/suspension.html.ejs
new file mode 100644
index 000000000..b36560ec5
--- /dev/null
+++ b/services/email/suspension.html.ejs
@@ -0,0 +1 @@
+<%= body %>
diff --git a/services/metadata.js b/services/metadata.js
new file mode 100644
index 000000000..2cabed8b8
--- /dev/null
+++ b/services/metadata.js
@@ -0,0 +1,80 @@
+/**
+ * The key must be composed of alpha characters with periods seperating them.
+ */
+const KEY_REGEX = /^(?:[A-Za-z][A-Za-z\.]*[A-Za-z])?(?:[A-Za-z]*)$/;
+
+/**
+ * Allows metadata properties to be set/unset from specific models. It is the
+ * expecatation of this API that the metadata field is either accessed later
+ * directly, or accessed as a result of another database load rather than
+ * this service providing an interface to do so.
+ *
+ * @class MetadataService
+ */
+class MetadataService {
+
+ /**
+ * Parses a key by ensuring that if it is either a string, or an array with
+ * only characters defined in the `KEY_REGEX`
+ *
+ * @static
+ * @param {String|Array} key
+ * @returns {String} string form of the key
+ *
+ * @memberOf Metadata
+ */
+ static parseKey(key) {
+ if (Array.isArray(key)) {
+ key = key.join('.');
+ }
+
+ if ((typeof key !== 'string') || !KEY_REGEX.test(key) || key.length === 0) {
+ throw new Error(`${key} is not valid, only a-zA-Z. allowed`);
+ }
+
+ return ['metadata', key].join('.');
+ }
+
+ /**
+ * Sets an object on the metadata field of an object.
+ *
+ * @static
+ * @param {mongoose.Model} model the mongoose model for the object
+ * @param {String} id the value for the field `id` of the model
+ * @param {String|Array} key key for the metadata field
+ * @param {any} value javascript object to set the value of the metadata to
+ * @returns {Promise} resolves when the update is complete
+ *
+ * @memberOf Metadata
+ */
+ static async set(model, id, key, value) {
+ key = MetadataService.parseKey(key);
+
+ return model.update({id}, {
+ $set: {
+ [key]: value
+ }
+ });
+ }
+
+ /**
+ * Removes the value for the metadata field as the specific key.
+ *
+ * @static
+ * @param {mongoose.Model} model the mongoose model for the object
+ * @param {String} id the value for the field `id` of the model
+ * @param {String|Array} key key for the metadata field
+ * @returns
+ *
+ * @memberOf Metadata
+ */
+ static async unset(model, id, key) {
+ key = MetadataService.parseKey(key);
+
+ return model.update({id}, {
+ $unset: {[key]: ''}
+ });
+ }
+}
+
+module.exports = MetadataService;