diff --git a/packages/vulcan-core/lib/modules/default_mutations.js b/packages/vulcan-core/lib/modules/default_mutations.js index 2f4d3a178..8aa451e47 100644 --- a/packages/vulcan-core/lib/modules/default_mutations.js +++ b/packages/vulcan-core/lib/modules/default_mutations.js @@ -4,121 +4,213 @@ Default mutations */ -import { newMutation, editMutation, removeMutation, Utils } from 'meteor/vulcan:lib'; +import { registerCallback, newMutation, editMutation, removeMutation, Utils } from 'meteor/vulcan:lib'; import Users from 'meteor/vulcan:users'; -export const getDefaultMutations = (collectionName, options = {}) => ({ +export const getDefaultMutations = (collectionName, options = {}) => { - // mutation for inserting a new document + // register callbacks for documentation purposes + registerCollectionCallbacks(collectionName); - new: { - - name: `${collectionName}New`, - - // check function called on a user to see if they can perform the operation - check(user, document) { - if (options.newCheck) { - return options.newCheck(user, document); - } - // if user is not logged in, disallow operation - if (!user) return false; - // else, check if they can perform "foo.new" operation (e.g. "movies.new") - return Users.canDo(user, `${collectionName.toLowerCase()}.new`); - }, - - async mutation(root, {document}, context) { + return { + + // mutation for inserting a new document + + new: { - const collection = context[collectionName]; - - // check if current user can pass check function; else throw error - Utils.performCheck(this.check, context.currentUser, document); - - // pass document to boilerplate newMutation function - return await newMutation({ - collection, - document: document, - currentUser: context.currentUser, - validate: true, - context, - }); - }, - - }, - - // mutation for editing a specific document - - edit: { - - name: `${collectionName}Edit`, - - // check function called on a user and document to see if they can perform the operation - check(user, document) { - if (options.editCheck) { - return options.editCheck(user, document); - } - - if (!user || !document) return false; - // check if user owns the document being edited. - // if they do, check if they can perform "foo.edit.own" action - // if they don't, check if they can perform "foo.edit.all" action - return Users.owns(user, document) ? Users.canDo(user, `${collectionName.toLowerCase()}.edit.own`) : Users.canDo(user, `${collectionName.toLowerCase()}.edit.all`); - }, - - async mutation(root, {documentId, set, unset}, context) { - - const collection = context[collectionName]; - - // get entire unmodified document from database - const document = collection.findOne(documentId); - - // check if user can perform operation; if not throw error - Utils.performCheck(this.check, context.currentUser, document); - - // call editMutation boilerplate function - return await editMutation({ - collection, - documentId: documentId, - set: set, - unset: unset, - currentUser: context.currentUser, - validate: true, - context, - }); - }, - - }, - - // mutation for removing a specific document (same checks as edit mutation) - - remove: { - - name: `${collectionName}Remove`, - - check(user, document) { - if (options.removeCheck) { - return options.removeCheck(user, document); - } + name: `${collectionName}New`, - if (!user || !document) return false; - return Users.owns(user, document) ? Users.canDo(user, `${collectionName.toLowerCase()}.remove.own`) : Users.canDo(user, `${collectionName.toLowerCase()}.remove.all`); + // check function called on a user to see if they can perform the operation + check(user, document) { + if (options.newCheck) { + return options.newCheck(user, document); + } + // if user is not logged in, disallow operation + if (!user) return false; + // else, check if they can perform "foo.new" operation (e.g. "movies.new") + return Users.canDo(user, `${collectionName.toLowerCase()}.new`); + }, + + async mutation(root, {document}, context) { + + const collection = context[collectionName]; + + // check if current user can pass check function; else throw error + Utils.performCheck(this.check, context.currentUser, document); + + // pass document to boilerplate newMutation function + return await newMutation({ + collection, + document: document, + currentUser: context.currentUser, + validate: true, + context, + }); + }, + + }, + + // mutation for editing a specific document + + edit: { + + name: `${collectionName}Edit`, + + // check function called on a user and document to see if they can perform the operation + check(user, document) { + if (options.editCheck) { + return options.editCheck(user, document); + } + + if (!user || !document) return false; + // check if user owns the document being edited. + // if they do, check if they can perform "foo.edit.own" action + // if they don't, check if they can perform "foo.edit.all" action + return Users.owns(user, document) ? Users.canDo(user, `${collectionName.toLowerCase()}.edit.own`) : Users.canDo(user, `${collectionName.toLowerCase()}.edit.all`); + }, + + async mutation(root, {documentId, set, unset}, context) { + + const collection = context[collectionName]; + + // get entire unmodified document from database + const document = collection.findOne(documentId); + + // check if user can perform operation; if not throw error + Utils.performCheck(this.check, context.currentUser, document); + + // call editMutation boilerplate function + return await editMutation({ + collection, + documentId: documentId, + set: set, + unset: unset, + currentUser: context.currentUser, + validate: true, + context, + }); + }, + }, - async mutation(root, {documentId}, context) { + // mutation for removing a specific document (same checks as edit mutation) - const collection = context[collectionName]; + remove: { - const document = collection.findOne(documentId); - Utils.performCheck(this.check, context.currentUser, document, context); + name: `${collectionName}Remove`, + + check(user, document) { + if (options.removeCheck) { + return options.removeCheck(user, document); + } + + if (!user || !document) return false; + return Users.owns(user, document) ? Users.canDo(user, `${collectionName.toLowerCase()}.remove.own`) : Users.canDo(user, `${collectionName.toLowerCase()}.remove.all`); + }, + + async mutation(root, {documentId}, context) { + + const collection = context[collectionName]; + + const document = collection.findOne(documentId); + Utils.performCheck(this.check, context.currentUser, document, context); + + return await removeMutation({ + collection, + documentId: documentId, + currentUser: context.currentUser, + validate: true, + context, + }); + }, - return await removeMutation({ - collection, - documentId: documentId, - currentUser: context.currentUser, - validate: true, - context, - }); }, + } - }, +}; -}); + +const registerCollectionCallbacks = collectionName => { + + collectionName = collectionName.toLowerCase(); + + registerCallback({ + name: `${collectionName}.new.validate`, + arguments: [{document: 'The document being inserted'}, {currentUser: 'The current user'}, {validationErrors: 'An object that can be used to accumulate validation errors'}], + runs: 'sync', + returns: 'document', + description: `Validate a document before insertion (can be skipped when inserting directly on server).` + }); + registerCallback({ + name: `${collectionName}.new.before`, + arguments: [{document: 'The document being inserted'}, {currentUser: 'The current user'}], + runs: 'sync', + returns: 'document', + description: `Perform operations on a new document before it's inserted in the database.` + }); + registerCallback({ + name: `${collectionName}.new.after`, + arguments: [{document: 'The document being inserted'}, {currentUser: 'The current user'}], + runs: 'sync', + returns: 'document', + description: `Perform operations on a new document after it's inserted in the database but *before* the mutation returns it.` + }); + registerCallback({ + name: `${collectionName}.new.async`, + arguments: [{document: 'The document being inserted'}, {currentUser: 'The current user'}, {collection: 'The collection the document belongs to'}], + runs: 'async', + returns: null, + description: `Perform operations on a new document after it's inserted in the database asynchronously.` + }); + + registerCallback({ + name: `${collectionName}.edit.validate`, + arguments: [{modifier: 'The MongoDB modifier'}, {document: 'The document being inserted'}, {currentUser: 'The current user'}, {validationErrors: 'An object that can be used to accumulate validation errors'}], + runs: 'sync', + returns: 'modifier', + description: `Validate a document before update (can be skipped when updating directly on server).` + }); + registerCallback({ + name: `${collectionName}.edit.before`, + arguments: [{modifier: 'The MongoDB modifier'}, {document: 'The document being inserted'}, {currentUser: 'The current user'}], + runs: 'sync', + returns: 'modifier', + description: `Perform operations on a document before it's updated in the database.` + }); + registerCallback({ + name: `${collectionName}.edit.after`, + arguments: [{document: 'The document being inserted'}, {currentUser: 'The current user'}], + runs: 'sync', + returns: 'document', + description: `Perform operations on a document after it's updated in the database but *before* the mutation returns it.` + }); + registerCallback({ + name: `${collectionName}.edit.async`, + arguments: [{document: 'The document being updated'}, {currentUser: 'The current user'}, {collection: 'The collection the document belongs to'}], + runs: 'async', + returns: null, + description: `Perform operations on a document after it's updated in the database asynchronously.` + }); + + registerCallback({ + name: `${collectionName}.remove.validate`, + arguments: [{document: 'The document being removed'}, {currentUser: 'The current user'}, {validationErrors: 'An object that can be used to accumulate validation errors'}], + runs: 'sync', + returns: 'document', + description: `Validate a document before removal (can be skipped when removing directly on server).` + }); + registerCallback({ + name: `${collectionName}.remove.before`, + arguments: [{document: 'The document being removed'}, {currentUser: 'The current user'}], + runs: 'sync', + returns: null, + description: `Perform operations on a document before it's removed from the database.` + }); + registerCallback({ + name: `${collectionName}.new.async`, + arguments: [{document: 'The document being removed'}, {currentUser: 'The current user'}, {collection: 'The collection the document belongs to'}], + runs: 'async', + returns: null, + description: `Perform operations on a document after it's removed from the database asynchronously.` + }); +} \ No newline at end of file diff --git a/packages/vulcan-debug/lib/components/Callbacks.jsx b/packages/vulcan-debug/lib/components/Callbacks.jsx new file mode 100644 index 000000000..b158d1f8f --- /dev/null +++ b/packages/vulcan-debug/lib/components/Callbacks.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { FormattedMessage } from 'meteor/vulcan:i18n'; +import { registerComponent, Components } from 'meteor/vulcan:lib'; +import Callbacks from '../modules/callbacks/collection.js'; + +const CallbacksName = ({ document }) => + {document.name} + +const CallbacksDashboard = props => +
+ +
+ +registerComponent('Callbacks', CallbacksDashboard); + +export default Callbacks; \ No newline at end of file diff --git a/packages/vulcan-debug/lib/modules/callbacks/collection.js b/packages/vulcan-debug/lib/modules/callbacks/collection.js new file mode 100644 index 000000000..ca30d4a7a --- /dev/null +++ b/packages/vulcan-debug/lib/modules/callbacks/collection.js @@ -0,0 +1,19 @@ +import { createCollection } from 'meteor/vulcan:lib'; +import schema from './schema.js'; +import resolvers from './resolvers.js'; +import './fragments.js'; + +const Callbacks = createCollection({ + + collectionName: 'Callbacks', + + typeName: 'Callback', + + schema, + + resolvers, + +}); + + +export default Callbacks; diff --git a/packages/vulcan-debug/lib/modules/callbacks/fragments.js b/packages/vulcan-debug/lib/modules/callbacks/fragments.js new file mode 100644 index 000000000..b0f706b5e --- /dev/null +++ b/packages/vulcan-debug/lib/modules/callbacks/fragments.js @@ -0,0 +1,12 @@ +import { registerFragment } from 'meteor/vulcan:lib'; + +registerFragment(` + fragment CallbacksFragment on Callback { + name + arguments + runs + returns + description + hooks + } +`); diff --git a/packages/vulcan-debug/lib/modules/callbacks/resolvers.js b/packages/vulcan-debug/lib/modules/callbacks/resolvers.js new file mode 100644 index 000000000..4d2326a9e --- /dev/null +++ b/packages/vulcan-debug/lib/modules/callbacks/resolvers.js @@ -0,0 +1,26 @@ +import { CallbackHooks } from 'meteor/vulcan:lib'; + +const resolvers = { + + list: { + + name: 'CallbacksList', + + resolver(root, {terms = {}}, context, info) { + return CallbackHooks; + }, + + }, + + total: { + + name: 'CallbacksTotal', + + resolver(root, {terms = {}}, context) { + return CallbackHooks.length; + }, + + } +}; + +export default resolvers; \ No newline at end of file diff --git a/packages/vulcan-debug/lib/modules/callbacks/schema.js b/packages/vulcan-debug/lib/modules/callbacks/schema.js new file mode 100644 index 000000000..86cc28bbe --- /dev/null +++ b/packages/vulcan-debug/lib/modules/callbacks/schema.js @@ -0,0 +1,63 @@ +import { Callbacks } from 'meteor/vulcan:lib'; + +const schema = { + + name: { + label: 'Name', + type: String, + viewableBy: ['admins'], + }, + + arguments: { + label: 'Arguments', + type: Array, + viewableBy: ['admins'], + }, + + 'arguments.$': { + type: Object, + viewableBy: ['admins'], + }, + + runs: { + label: 'Runs', + type: String, + viewableBy: ['admins'], + }, + + returns: { + label: 'Should Return', + type: String, + viewableBy: ['admins'], + }, + + description: { + label: 'Description', + type: String, + viewableBy: ['admins'], + }, + + hooks: { + label: 'Hooks', + type: Array, + viewableBy: ['admins'], + resolveAs: { + type: '[String]', + resolver: (callback) => { + console.log('// callback') + console.log(callback) + console.log(Callbacks[callback.name]) + if (Callbacks[callback.name]) { + const callbacks = Callbacks[callback.name].map(f => f.name); + console.log(callbacks) + return callbacks; + } else { + return []; + } + } + } + } + +}; + +export default schema; diff --git a/packages/vulcan-debug/lib/modules/components.js b/packages/vulcan-debug/lib/modules/components.js index 4e61994fe..017a9ea69 100644 --- a/packages/vulcan-debug/lib/modules/components.js +++ b/packages/vulcan-debug/lib/modules/components.js @@ -2,3 +2,4 @@ import '../components/Emails.jsx'; import '../components/Groups.jsx'; import '../components/Settings.jsx'; +import '../components/Callbacks.jsx'; diff --git a/packages/vulcan-debug/lib/modules/routes.js b/packages/vulcan-debug/lib/modules/routes.js index f1b85b835..1517f8590 100644 --- a/packages/vulcan-debug/lib/modules/routes.js +++ b/packages/vulcan-debug/lib/modules/routes.js @@ -4,6 +4,7 @@ addRoute([ // {name: 'cheatsheet', path: '/cheatsheet', component: import('./components/Cheatsheet.jsx')}, {name: 'groups', path: '/groups', component: () => getDynamicComponent(import('../components/Groups.jsx'))}, {name: 'settings', path: '/settings', componentName: 'Settings'}, + {name: 'settings', path: '/callbacks', componentName: 'Callbacks'}, // {name: 'emails', path: '/emails', component: () => getDynamicComponent(import('./components/Emails.jsx'))}, {name: 'emails', path: '/emails', componentName: 'Emails'}, ]); \ No newline at end of file diff --git a/packages/vulcan-lib/lib/modules/callbacks.js b/packages/vulcan-lib/lib/modules/callbacks.js index 698699271..26ba1a71a 100644 --- a/packages/vulcan-lib/lib/modules/callbacks.js +++ b/packages/vulcan-lib/lib/modules/callbacks.js @@ -1,11 +1,26 @@ import { debug } from './debug.js'; +/** + * @summary A list of all registered callback hooks + */ +export const CallbackHooks = []; + /** * @summary Callback hooks provide an easy way to add extra steps to common operations. * @namespace Callbacks */ export const Callbacks = {}; + +/** + * @summary Register a callback + * @param {String} hook - The name of the hook + * @param {Function} callback - The callback function + */ +export const registerCallback = function (callback) { + CallbackHooks.push(callback); +}; + /** * @summary Add a callback function to a hook * @param {String} hook - The name of the hook diff --git a/packages/vulcan-lib/lib/server/mutations.js b/packages/vulcan-lib/lib/server/mutations.js index 01eb5efd7..9c3acd816 100644 --- a/packages/vulcan-lib/lib/server/mutations.js +++ b/packages/vulcan-lib/lib/server/mutations.js @@ -220,6 +220,7 @@ export const removeMutation = async ({ collection, documentId, currentUser, vali } } + await runCallbacks(`${collectionName}.remove.before`, document, currentUser); await runCallbacks(`${collectionName}.remove.sync`, document, currentUser); collection.remove(documentId);