RFC: specific server-side state mutations
Please return to your seats and fasten your seatbelts. There may be some turbulence ahead.
Definitions
- Server-side state is essentially the data in the database.
- Server-side state mutations is anything that allows the client to modify the server-side state. Currently this is the REST PUT, PATCH, POST & DELETE endpoints and the GraphQL mutations.
Problem
At present we have few, generic server-side state mutations. This has several drawbacks:
- The authsome mode has to respond to very generic questions like "can this user update this document with this arbitrary selection of fields and values". This is hard to reason about and requires extra logic to filter and block mutations which attempt to do weird things like changing the creation date of a document.
- It is not obvious where to put server-side business logic. Either it goes in the authsome mode or you create a specific server-side mutation just for this case. Examples of business logic would be rules which apply equally to all roles such as "a manuscript cannot be deleted, only archived" or "a user cannot simultaneously change their email address and password". In xPub, a lot of business logic has just gone in the client with a TODO saying "this should be on the server".
- If business logic goes into the authsome mode it is lost to resuability since the mode is not reusable.
- Some business logic, such as sending an email notification, cannot go into the authsome mode. With the proposed notification subsystem one can imagine that the email could be triggered by a rule that watches for database updates. But some user actions result in multiple database interactions (e.g. requesting a revision touches the manuscript, the version and create a new version). What if you need to send an email only when two or more specific events are encountered? Doable but complicated.
- For audit logging, it's important to show events grouped into meaningful units. Reconstructing "revision requested" from the individual database updates would be practically impossible and there's nowhere on the server to trigger such a meta-event. Similarly problems apply to performance monitoring and forensic analysis of logs.
- With only generic server side mutations it's impossible to use database transactions for atomic updates across multiple documents. What if inserting the new manuscript version fails but marking the current one as read-only succeeds?
- Finally on a conceptual level, it's strange that we've embraced atomic, composable, components on the client side, with each React component encapsulating its own display and business logic, but shunned it on the server, with almost all server interaction funnelled through the same core mutations.
Suggestion
Componentize server-side state mutations.
Most organism-level React components would have a corresponding Pubsweet component defining the actions that go with that organism. These actions map to the specific functionality of the React component: the upload manuscript organism has an "upload manuscript" action, the submission form has "save progress" and "submit manuscript" actions, the decision component has a "make decision" action etc. These actions constitute both a client-side function (think redux action or Apollo mutation) and the server side functionality to back that action (think REST endpoint or GraphQL resolvers).
It remains the responsibility of the app-specific container component to wire together the UI components and the actions (see #336 (closed)) so developers have the option of using each on their own but the expectation is that the vast majority of cases would use both together.
The code for a server mutation with some simple business logic would look like:
module.exports = {
typeDefs: `extend Mutation { submitDraft($id: ID): Fragment }`,
resolvers: {
Mutation: {
submitDraft: async (_, { id }, ctx) => {
const fragment = await ctx.connectors.fragments.find(id)
if (fragment.status !== 'draft') {
throw new InvalidRequestError('Cannot submit non-draft manuscript')
}
if (!authsome.can(ctx.user, 'submit draft', { fragment })) {
throw new NotPermittedError()
}
return ctx.connectors.fragments.update(id, { status: 'submitted' })
},
},
},
}
Concerns
- Writing these specific server side mutations will likely result in more code overall, but the authsome mode will be smaller and much simpler.
- Removing the generic state mutations from
pubsweet-server
leaves it being nothing more than a set of conventions and glue that allow the other components to work together. One could say this is already its most important feature and emphasising that fact is no bad thing. - Creating all these small mutations in different packages dispersese the business logic instead of having it centralised in the authsome mode. But I propose that this makes the code easier to understand and maintain. Instead of every piece of functionality being split between the client side action, server side mutation, authsome mode and event brokering code, each piece of functionality ends up in its own place, easily located by name.
References
You may be tempted to create a mutation like sendEmail(type: PASSWORD_RESET) and call this mutation with all the different email types you may have. This is not a good design because it will be much more difficult to enforce the correct input in the GraphQL type system and understand what operations are available in GraphiQL. [...]
Don’t be afraid of super specific mutations that correspond exactly to an update that your UI can make. Specific mutations that correspond to semantic user actions are more powerful than general mutations. This is because specific mutations are easier for a UI developer to write, they can be optimized by a backend developer, and only providing a specific subset of mutations makes it much harder for an attacker to exploit your API.