Mutations
This is a new way to think about how you deal with mutations in Meteor. (a very opinionated way!)
We go with Meteor on the road of CQRS and we want to separate fully the way we deal with fetching data and performing mutations.
This package heavily recommends Grapher for doing the heavy lifting of data fetching. Even if you do not use MongoDB, you can use Grapher's queries for anything that you wish by using Resolver Queries
Advantages that this approach offers:
- Separates concerns for methods and avoids using strings, rather as modules
- Easy separation between client-side code and server-side
- Uses promise-only approach
- Provides great logging for development to see what's going on
- Powerful ability to extend mutations by adding hooks before and after calling and execution
How does it compare to ValidatedMethod ?
- Easily separates server code from client code
Defining our mutations
This approach aims at bringing a little bit of structure into your mutations:
1// file: /imports/api/mutations/defs.js 2import { wrap, Mutation } from 'meteor/cultofcoders:mutations'; 3 4export const todoAdd = wrap({ 5 name: 'todos.add', 6 params: { 7 listId: String, 8 todo: Object, 9 }, 10}); 11 12export const todoRemove = new Mutation({ 13 name: 'todos.remove', 14 params: { 15 todoId: String, 16 }, 17}); 18 19// wrap(config) === new Mutation(config) 20// Just different syntax
Params should be in the form of something that check can handle. Always use params as objects when you send out to methods for clarity.
Adding handler to our mutation
1// you can put this SERVER ONLY or SERVER AND CLIENT 2// file: /imports/api/mutations/index.js 3 4import { todoAdd, todoRemove } from './defs'; 5 6// notice that now the context is pass as an argument, no longer have to use `this.userId` 7todoAdd.setHandler((context, params) => { 8 const { listId, todo } = params; 9 // ALWAYS DELEGATE. MUTATIONS SHOULD BE DUMB! 10 return TodoService.addTodo(context.userId, listId, todo); 11});
If your mutations are many, and as your app grows that would be the case, decouple them into separate files,
based on the concern they're tackling like: /imports/api/mutations/items.js
Calling our mutations
1// CLIENT 2import { todoAdd, todoRemove } from '...'; 3 4// somewhere in your frontend layer... 5onAddButton() { 6 const todo = this.getTodo(); 7 8 todoAdd.run({ 9 listId: this.props.listId, 10 todo, 11 }).then((todoId) => { 12 13 }) 14}
Logging
For convenience, we'll show in the console
the what calls are made and what responses are received, so you have full visibility.
By default, if Meteor.isDevelopment
is true
, logging is enabled , to disable this functionality.
1import { Mutation } from 'meteor/cultofcoders:mutations'; 2 3Mutation.isDebugEnabled = false;
Hook into the package!
The beauty of this package is that it allows you to hook into the befores and afters of the calls.
For example:
1import { Mutation } from 'meteor/cultofcoders:mutations'; 2 3// Runs 1st 4Mutation.addBeforeCall(({ config, params }) => { 5 // Do something before calling the mutation (most likely on client) 6}); 7 8// Runs 2nd 9Mutation.addBeforeExecution(({ context, config, params }) => { 10 // Do something before executing the mutation (most likely on server) 11 // Maybe run some checks based on some custom configs ? 12}); 13 14// Runs 3rd 15Mutation.addAfterExecution(({ context, config, params, result, error }) => { 16 // Do something after mutation has been executed (most likely on server) 17 // You could log the errors and send them somewhere? 18}); 19 20// Runs 4th 21Mutation.addAfterCall(({ config, params, result, error }) => { 22 // Do something after response has been returned (most likely on client) 23 // Maybe do some logging or dispatch an event? Maybe useful with Redux? 24});
This allows us to do some nice things like, here's some wild examples:
1// file: /imports/api/mutations/defs.js 2export const todoAdd = wrap({ 3 name: 'todo_add', 4 params: { 5 listId: String, 6 todo: Object, 7 }, 8 9 // Your own convention of permission checking 10 permissions: [ListPermissions.ADMIN], 11 12 // Set expectations of what the response would look like 13 // This makes it very easy for frontend devs to just look at the def and know how to use this 14 expect: { 15 todoId: String, 16 }, 17 18 // Inject data into params based on a parameter 19 provider: Providers.LIST, 20};
1// file: /imports/api/mutations/aop/permissions.js 2Mutation.addBeforeExecution(function permissionCheck({ 3 context, 4 config, 5 params 6}) => { 7 const {permissions} = config; 8 const {listId} = params; 9 10 if (permissions) { 11 // throw exception if listId is undefined 12 ListPermissionService.runChecks(context.userId, listId, permissions); 13 } 14}) 15 16// Optionally add the expect AOP to securely write methods and prevent from returning bad data 17Mutation.addAfterExecution(function expectCheck({ 18 config, 19 result 20}) { 21 if (config.expect) { 22 check(result, config.expect); 23 } 24}) 25 26// file: /imports/api/mutations/aop/provider.js 27Mutation.addBeforeExecution(function provider({ 28 context, 29 config, 30 params 31}) => { 32 if (config.provider == Providers.LIST) { 33 // Inject it in params, so the handler can access it from there 34 params.list = Lists.findOne(listId); 35 } 36})
You can also hook into individual mutations having the same exact syntax:
1addTodo.addBeforeCall(function () { ... }); 2addTodo.addBeforeExecutionCall(function () { ... }); 3addTodo.addAfterExecutionCall(function () { ... }); 4addTodo.addAfterCall(function () { ... });
What happens if you have 100+ methods ?
Decouple, but keep them centralized in, keep api/mutations/defs/index.js
as an aggregator:
1// /imports/api/mutations/defs/index.js 2 3export { todoAdd, todoRemove, todoEdit } from './todo.defs.js'; 4export { todoAdd, todoRemove, todoEdit } from './list.defs.js';
Feel free to submit an issue if you have any ideas for improvement!