Vulcan/packages/vulcan-lib/lib/server/mutations.js

239 lines
7.6 KiB
JavaScript
Raw Normal View History

2016-11-07 12:59:39 +09:00
/*
Mutations have four steps:
1. Validation
2017-02-06 21:47:27 +08:00
If the mutation call is not trusted (i.e. it comes from a GraphQL mutation),
we'll run all validate steps:
- Check that the current user has permission to insert/edit each field.
- Add userId to document (insert only).
- Run validation callbacks.
2016-11-07 12:59:39 +09:00
2. Sync Callbacks
2017-02-06 21:47:27 +08:00
The second step is to run the mutation argument through all the sync callbacks.
2016-11-07 12:59:39 +09:00
3. Operation
We then perform the insert/update/remove operation.
4. Async Callbacks
2017-02-06 21:47:27 +08:00
Finally, *after* the operation is performed, we execute any async callbacks.
2016-11-07 12:59:39 +09:00
Being async, they won't hold up the mutation and slow down its response time
2017-02-06 21:47:27 +08:00
to the client.
2016-11-07 12:59:39 +09:00
*/
import { runCallbacks, runCallbacksAsync } from '../modules/index.js';
import { createError } from 'apollo-errors';
import { validateDocument, validateModifier } from '../modules/validation.js';
2017-09-14 19:47:35 +02:00
import { debug } from '../modules/debug.js';
export const newMutation = async ({ collection, document, currentUser, validate, context }) => {
2017-02-06 21:47:27 +08:00
2017-09-14 19:47:35 +02:00
debug('//------------------------------------//');
debug('// newMutation');
debug(collection._name);
2017-09-15 11:14:09 +02:00
debug(`validate: ${validate}`);
2017-09-14 19:47:35 +02:00
debug(document);
2017-02-06 21:47:27 +08:00
// we don't want to modify the original document
let newDocument = Object.assign({}, document);
const collectionName = collection._name;
const schema = collection.simpleSchema()._schema;
if (validate) {
const validationErrors = validateDocument(newDocument, collection, context);
// run validation callbacks
newDocument = runCallbacks(`${collectionName}.new.validate`, newDocument, currentUser, validationErrors);
if (validationErrors.length) {
const NewDocumentValidationError = createError('app.validation_error', {message: 'app.new_document_validation_error'});
throw new NewDocumentValidationError({data: {break: true, errors: validationErrors}});
}
}
// check if userId field is in the schema and add it to document if needed
const userIdInSchema = Object.keys(schema).find(key => key === 'userId');
if (!!userIdInSchema && !newDocument.userId) newDocument.userId = currentUser._id;
// run onInsert step
2017-08-30 09:38:05 +09:00
// note: cannot use forEach with async/await.
// See https://stackoverflow.com/a/37576787/649299
for(let fieldName of _.keys(schema)) {
if (schema[fieldName].onInsert) {
2017-08-30 09:38:05 +09:00
const autoValue = await schema[fieldName].onInsert(newDocument, currentUser);
2017-09-23 08:10:58 +02:00
if (typeof autoValue !== 'undefined') {
newDocument[fieldName] = autoValue;
}
}
2017-08-30 09:38:05 +09:00
}
// TODO: find that info in GraphQL mutations
// if (Meteor.isServer && this.connection) {
// post.userIP = this.connection.clientAddress;
2017-09-14 19:47:35 +02:00
// post.userAgent = this.connection.httpHeaders['user-agent'];
// }
// run sync callbacks
newDocument = await runCallbacks(`${collectionName}.new.before`, newDocument, currentUser);
newDocument = await runCallbacks(`${collectionName}.new.sync`, newDocument, currentUser);
// add _id to document
newDocument._id = collection.insert(newDocument);
2016-11-07 12:59:39 +09:00
// run any post-operation sync callbacks
newDocument = await runCallbacks(`${collectionName}.new.after`, newDocument, currentUser);
2016-11-07 12:59:39 +09:00
// get fresh copy of document from db
// TODO: not needed?
const insertedDocument = collection.findOne(newDocument._id);
// run async callbacks
// note: query for document to get fresh document with collection-hooks effects applied
await runCallbacksAsync(`${collectionName}.new.async`, insertedDocument, currentUser, collection);
2017-02-06 21:47:27 +08:00
2017-09-14 19:47:35 +02:00
debug('// new mutation finished:');
debug(newDocument);
debug('//------------------------------------//');
2017-02-06 21:47:27 +08:00
return newDocument;
}
export const editMutation = async ({ collection, documentId, set = {}, unset = {}, currentUser, validate, context }) => {
2017-02-06 21:47:27 +08:00
const collectionName = collection._name;
const schema = collection.simpleSchema()._schema;
// build mongo modifier from arguments
let modifier = {$set: set, $unset: unset};
// get original document from database
let document = collection.findOne(documentId);
2017-10-05 09:14:42 +09:00
debug('//------------------------------------//');
debug('// editMutation');
debug('// collectionName: ', collection._name);
debug('// documentId: ', documentId);
// debug('// set: ', set);
// debug('// unset: ', unset);
// debug('// document: ', document);
if (validate) {
2017-08-19 16:18:18 +09:00
const validationErrors = validateModifier(modifier, document, collection, context);
modifier = runCallbacks(`${collectionName}.edit.validate`, modifier, document, currentUser, validationErrors);
if (validationErrors.length) {
console.log('// validationErrors')
console.log(validationErrors)
const EditDocumentValidationError = createError('app.validation_error', {message: 'app.edit_document_validation_error'});
throw new EditDocumentValidationError({data: {break: true, errors: validationErrors}});
}
}
// run onEdit step
2017-08-30 09:38:05 +09:00
for(let fieldName of _.keys(schema)) {
if (schema[fieldName].onEdit) {
2017-08-30 09:38:05 +09:00
const autoValue = await schema[fieldName].onEdit(modifier, document, currentUser);
if (typeof autoValue !== 'undefined') {
if (autoValue === null) {
// if any autoValue returns null, then unset the field
modifier.$unset[fieldName] = true;
} else {
modifier.$set[fieldName] = autoValue;
2017-09-05 15:45:14 +02:00
// make sure we don't try to unset the same field at the same time
delete modifier.$unset[fieldName];
}
}
}
2017-08-30 09:38:05 +09:00
}
// run sync callbacks (on mongo modifier)
modifier = await runCallbacks(`${collectionName}.edit.before`, modifier, document, currentUser);
modifier = await runCallbacks(`${collectionName}.edit.sync`, modifier, document, currentUser);
// remove empty modifiers
if (_.isEmpty(modifier.$set)) {
delete modifier.$set;
}
if (_.isEmpty(modifier.$unset)) {
delete modifier.$unset;
}
// update document
collection.update(documentId, modifier, {removeEmptyStrings: false});
// get fresh copy of document from db
let newDocument = collection.findOne(documentId);
// clear cache if needed
if (collection.loader) {
collection.loader.clear(documentId);
}
2017-04-15 21:39:36 +09:00
// run any post-operation sync callbacks
newDocument = await runCallbacks(`${collectionName}.edit.after`, newDocument, document, currentUser);
// run async callbacks
await runCallbacksAsync(`${collectionName}.edit.async`, newDocument, document, currentUser, collection);
2017-09-14 19:47:35 +02:00
debug('// edit mutation finished')
2017-10-05 09:14:42 +09:00
debug('// modifier: ', modifier)
debug('// edited document: ', newDocument)
2017-09-14 19:47:35 +02:00
debug('//------------------------------------//');
return newDocument;
}
export const removeMutation = async ({ collection, documentId, currentUser, validate, context }) => {
2017-09-14 19:47:35 +02:00
debug('//------------------------------------//');
debug('// removeMutation')
debug(collection._name)
debug(documentId)
debug('//------------------------------------//');
2016-11-07 12:59:39 +09:00
const collectionName = collection._name;
const schema = collection.simpleSchema()._schema;
2016-11-07 12:59:39 +09:00
2016-11-07 13:00:09 +09:00
let document = collection.findOne(documentId);
2016-11-07 12:59:39 +09:00
// if document is not trusted, run validation callbacks
if (validate) {
document = runCallbacks(`${collectionName}.remove.validate`, document, currentUser);
2016-11-07 12:59:39 +09:00
}
// run onRemove step
2017-08-30 09:38:05 +09:00
for(let fieldName of _.keys(schema)) {
if (schema[fieldName].onRemove) {
2017-08-30 09:38:05 +09:00
await schema[fieldName].onRemove(document, currentUser);
}
2017-08-30 09:38:05 +09:00
}
2017-10-21 15:58:02 +09:00
await runCallbacks(`${collectionName}.remove.before`, document, currentUser);
await runCallbacks(`${collectionName}.remove.sync`, document, currentUser);
2016-11-07 12:59:39 +09:00
collection.remove(documentId);
// clear cache if needed
if (collection.loader) {
collection.loader.clear(documentId);
}
await runCallbacksAsync(`${collectionName}.remove.async`, document, currentUser, collection);
return document;
}