Rethinking Mutations - Just some ideas


#1

Some wild thoughts, rethinking mutations, this was scrapped in one of my Sublime Text tabs for a while, but I’m curious to hear thoughts before actually implementing this thing. Warning, it’s super opinionated.

Before reading this here is my thinking of the evolution of Meteor:

  • As Grapher becomes more and more mature, it will eventually replace all your methods and publications that deliver data. Ok maybe you have some custom publications (like Redis Oplog Vent) that cannot be replaced, but most can.
  • After this transition to Grapher is made, most of your methods that you define will be used for performing mutations (I suggest to never mix methods for mutating and fetching data at once, rather split it in 2)
  • Methods should act as proxies, they should call other (static/singleton) services and the methods body should have maximum 4 statements. (1 or 2 for security checks, 1 or 2 for calling and returning the result from other service, they must not hold logic!)
  • Based on the assumptions above, we need to rethink mutations as a layer over our API, and we need to avoid strings as much as we can, and hide as much server-side code as we can. (This is where Validated Methods fails to convince me) It’s dangerous to expose Server-Side Code because you make your code way more easy to hack.

Define our mutations as a separate layer, outside /api:

// file: /imports/mutations/defs/posts.js
export const POSTS_ADD = {
	name: 'posts.add',
	describe: 'Add a post', // this would be helpful because we may need it when we see our web
	params: { // just use something like check, all params are objs
		text: String,
	}
};
// /imports/mutations/defs/index.js
// export everything from our definitions
export {} from './posts';
mutations/server/posts.js

import {POST_ADD, POST_REMOVE} from '../defs';
import {mutation, checkLoggedIn} from '...';

// always inject userId as first param in the run function, allowing you to not need context in most cases
mutation(POST_ADD, [checkLoggedIn, (userId, params) => {
	PostService.addPost(postId, data);
}])

// annotation ??
@mutation(POST_ADD)
function (userId, {postId}) {
	
}
// looks weird I know
mutations(
    POST_ADD, PostService.addPost,
    POST_REMOVE, PostService.removePost,
    [checkLoggedIn, checkIsAdmin]
)

Concepts, each mixin receives userId and params and can store additional data inside params. So, these additional functions act as firewalls and data-providers.

Now moving to the client, go with Promise-only approach:

import {POST_ADD, POST_REMOVE} from '/imports/mutations/defs';

// always use promises
mutate(POST_ADD, {postId}).then(() => {})
setMutateDefaults({
	before(def, params) {
		if (Meteor.isDevelopment) {
			console.log(`Calling method: ${def.name} with params:`, params);
		}
	},
	catch(err) {
		// ... some default behavior, maybe trigger a Notification somehow, and encourage him to submit a bug?
	},
	after(error, {def, params, result}) {
		if (Meteor.isDevelopment) {
			console.log(`Received from method: ${def.name}:`, result);
		}
	}
})

Now the real question… what the heck do we win ? Is this just another sugar coating bs ?

Here is what I think we win:

  • Easy abstraction of methods, without relying on knowing a string (autocompletion from IDE)
  • Forces you to validate params if you send params.
  • Doesn’t expose server-side code (but if you want opitmistic ui, you can just define it in shared env)
  • Easy method logger client side (no longer relying on websocket inspection)
  • Forces you to use promises.
  • The plan is to have it somehow integrated with Grapher Live where you see all your mutations, their params and available queries.

So here’s where I’m heading. I want Backend Developers to focus on writing tests, mutations, services, queries, db models. And then somehow something like Grapher Live to automatically generate docs for the allowed mutations with their params, the queries you can do, and the frontend developers just code React/Blaze/Vue/whatever, as they already have an API which lets them test it fast.

Ideas, critiques, suggestions welcome.

Cheers.


#2

I really like this idea! We talked about this with Theodor, who mentioned this has a redux feel to it, which made it click for me why I think this is a great approach. The abstraction of meteor methods is something I’ve tried to do on my own, but never built a good one.

Mixing this with recompose and something like createMutator (if not already doing this) for nice containers:

// PostContainer
import { mapProps } from 'recompose';
import { createMutator, mutate } from 'meteor-mutations';
import { POST_ADD, POST_REMOVE } from '/imports/mutations/defs';

export default (component) => mapProps(({ postId }) => ({
  // createMutator helper
  handleAddPost: createMutator(POST_ADD),

  // or mutate function
  handleRemovePost: () => mutate(POST_REMOVE, { postId }),
}))(component)

Would love to see more people chime in! :slight_smile:


#3

It’s done. Hope you will enjoy it. We will begin using it widely, as we are currently working very hard of creating an Enterprise Meteor Boilerplate, this will be a standard inside it.