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/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/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;