cultofcoders:mutations

v0.1.1Published 6 years ago

This package has not had recent updates. Please investigate it's current state before committing to using it in your project.

Mutations

Build Status

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!