cultofcoders:mutations

v0.0.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
  • Going on this modular approach allows easy separation of code server and client
  • Goes only promise-only approach
  • Provides great logging for development to see what's going on
  • Embedded and very extensible mutations using AOP

Defining our mutations

This approach aims at bringing a little bit of structure into your mutations:

1// file: /imports/api/mutations/defs.js
2export const TODO_ADD = {
3    name: 'todo_add',
4    params: {
5        listId: String,
6        todo: Object,
7    },
8};
9
10export const TODO_REMOVE = {
11    name: 'todo_remove',
12    params: {
13        todoId: String,
14    },
15};

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 { TODO_ADD, TODO_REMOVE } from './defs';
5import { mutation } from 'meteor/cultofcoders:mutations';
6
7// notice that now the context is pass as an argument, no longer have to use `this.userId`
8mutation(TODO_ADD, (context, params) => {
9    const { listId, todo } = params;
10    // ALWAYS DELEGATE. MUTATIONS SHOULD BE DUMB!
11    return TodoService.addTodo(context.userId, listId, todo);
12});

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 { TODO_ADD, TODO_REMOVE } from '...';
3import { mutate } from 'meteor/cultofcoders:mutations';
4
5
6// somewhere in your frontend layer...
7onAddButton() {
8    const todo = this.getTodo();
9
10    mutate(TODO_ADD, {
11        listId: this.props.listId,
12        todo,
13    }).then((todoId) => {
14
15    })
16}

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 { disableDebugging } from 'meteor/cultofcoders:mutations';
2
3disableDebugging();

AOP

The beauty of this package is that it allows you to hook into the befores and afters of the calls.

For example:

1import { mutateAOP, mutationAOP } from 'meteor/cultofcoders:mutations';
2
3mutateAOP.addBefore(({ config, params }) => {
4    // Do something before calling the mutation (most likely on client)
5});
6
7mutateAOP.addAfter(({ config, params, result, error }) => {
8    // Do something after response has been returned (most likely on client)
9    // Maybe do some logging or dispatch an event? Maybe useful with Redux?
10});
11
12mutationAOP.addBefore(({ context, config, params }) => {
13    // Do something before executing the mutation (most likely on server)
14    // Maybe run some checks based on some custom configs ?
15});
16
17mutationAOP.addAfter(({ context, config, params, result, error }) => {
18    // Do something after mutation has been executed (most likely on server)
19    // You could log the errors and send them somewhere?
20});

This allows us to do some nice things like, here's some wild examples:

1// file: /imports/api/mutations/defs.js
2export const TODO_ADD = {
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
2mutationAOP.addBefore(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
17mutationAOP.addAfter(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
27mutationAOP.addBefore(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})

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 { TODO_ADD, TODO_REMOVE, TODO_EDIT } from './todo.defs.js';
4export { LIST_ADD, LIST_REMOVE, LIST_EDIT } from './list.defs.js';

Feel free to submit an issue if you have any ideas for improvement!