-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mutation results #320
Mutation results #320
Changes from all commits
71934a6
a896130
f9a89e2
e49d027
f9eec6b
6fc9190
ebbd0f4
d59657b
14f8d29
e24a4d5
c2f5cae
696ecad
de3fe00
db03820
77f2fc4
1268180
61d4c2e
2ac9b67
d5ac174
c851979
034b8b7
3ec8ec3
54b6c33
8598615
4f989ba
a7b4e81
8ad1999
80c2df1
0362626
2eca76d
36d4d1c
08c258d
1f993be
528df4c
41c67bf
0c68047
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,263 @@ | ||
import { | ||
NormalizedCache, | ||
} from './store'; | ||
|
||
import { | ||
GraphQLResult, | ||
SelectionSet, | ||
} from 'graphql'; | ||
|
||
import mapValues = require('lodash.mapvalues'); | ||
import isArray = require('lodash.isarray'); | ||
import cloneDeep = require('lodash.clonedeep'); | ||
import assign = require('lodash.assign'); | ||
|
||
import { | ||
FragmentMap, | ||
} from '../queries/getFromAST'; | ||
|
||
import { | ||
scopeSelectionSetToResultPath, | ||
scopeJSONToResultPath, | ||
StorePath, | ||
} from './scopeQuery'; | ||
|
||
import { | ||
ApolloReducerConfig, | ||
} from '../store'; | ||
|
||
import { | ||
writeSelectionSetToStore, | ||
} from './writeToStore'; | ||
|
||
// Mutation behavior types, these can be used in the `resultBehaviors` argument to client.mutate | ||
|
||
export type MutationBehavior = | ||
MutationArrayInsertBehavior | | ||
MutationArrayDeleteBehavior | | ||
MutationDeleteBehavior; | ||
|
||
export type MutationArrayInsertBehavior = { | ||
type: 'ARRAY_INSERT'; | ||
resultPath: StorePath; | ||
storePath: StorePath; | ||
where: ArrayInsertWhere; | ||
} | ||
|
||
export type MutationDeleteBehavior = { | ||
type: 'DELETE'; | ||
dataId: string; | ||
} | ||
|
||
export type MutationArrayDeleteBehavior = { | ||
type: 'ARRAY_DELETE'; | ||
storePath: StorePath; | ||
dataId: string; | ||
} | ||
|
||
export type ArrayInsertWhere = | ||
'PREPEND' | | ||
'APPEND'; | ||
|
||
// These are the generic arguments passed into the mutation result reducers | ||
// The `behavior` field is specific to each reducer | ||
export type MutationBehaviorReducerArgs = { | ||
behavior: MutationBehavior; | ||
result: GraphQLResult; | ||
variables: any; | ||
fragmentMap: FragmentMap; | ||
selectionSet: SelectionSet; | ||
config: ApolloReducerConfig; | ||
} | ||
|
||
export type MutationBehaviorReducerMap = { | ||
[type: string]: MutationBehaviorReducer; | ||
} | ||
|
||
export type MutationBehaviorReducer = (state: NormalizedCache, args: MutationBehaviorReducerArgs) => NormalizedCache; | ||
|
||
// Reducer for ARRAY_INSERT behavior | ||
function mutationResultArrayInsertReducer(state: NormalizedCache, { | ||
behavior, | ||
result, | ||
variables, | ||
fragmentMap, | ||
selectionSet, | ||
config, | ||
}: MutationBehaviorReducerArgs): NormalizedCache { | ||
const { | ||
resultPath, | ||
storePath, | ||
where, | ||
} = behavior as MutationArrayInsertBehavior; | ||
|
||
// Step 1: get selection set and result for resultPath | ||
const scopedSelectionSet = scopeSelectionSetToResultPath({ | ||
selectionSet, | ||
fragmentMap, | ||
path: resultPath, | ||
}); | ||
|
||
const scopedResult = scopeJSONToResultPath({ | ||
json: result.data, | ||
path: resultPath, | ||
}); | ||
|
||
// OK, now we need to get a dataID to pass to writeSelectionSetToStore | ||
const dataId = config.dataIdFromObject(scopedResult) || generateMutationResultDataId(); | ||
|
||
// Step 2: insert object into store with writeSelectionSet | ||
state = writeSelectionSetToStore({ | ||
result: scopedResult, | ||
dataId, | ||
selectionSet: scopedSelectionSet, | ||
store: state, | ||
variables, | ||
dataIdFromObject: config.dataIdFromObject, | ||
fragmentMap, | ||
}); | ||
|
||
// Step 3: insert dataId reference into storePath array | ||
const dataIdOfObj = storePath.shift(); | ||
const clonedObj = cloneDeep(state[dataIdOfObj]); | ||
const array = scopeJSONToResultPath({ | ||
json: clonedObj, | ||
path: storePath, | ||
}); | ||
|
||
if (where === 'PREPEND') { | ||
array.unshift(dataId); | ||
} else if (where === 'APPEND') { | ||
array.push(dataId); | ||
} else { | ||
throw new Error('Unsupported "where" option to ARRAY_INSERT.'); | ||
} | ||
|
||
return assign(state, { | ||
[dataIdOfObj]: clonedObj, | ||
}) as NormalizedCache; | ||
} | ||
|
||
// Helper for ARRAY_INSERT. | ||
// When writing query results to the store, we generate IDs based on their path in the query. Here, | ||
// we don't have access to such uniquely identifying information, so the best we can do is a | ||
// sequential ID. | ||
let currId = 0; | ||
function generateMutationResultDataId() { | ||
currId++; | ||
return `ARRAY_INSERT-gen-id-${currId}`; | ||
} | ||
|
||
// Reducer for 'DELETE' behavior | ||
function mutationResultDeleteReducer(state: NormalizedCache, { | ||
behavior, | ||
}: MutationBehaviorReducerArgs): NormalizedCache { | ||
const { | ||
dataId, | ||
} = behavior as MutationDeleteBehavior; | ||
|
||
// Delete the object | ||
delete state[dataId]; | ||
|
||
// Now we need to go through the whole store and remove all references | ||
const newState = mapValues(state, (storeObj) => { | ||
return removeRefsFromStoreObj(storeObj, dataId); | ||
}); | ||
|
||
return newState; | ||
} | ||
|
||
function removeRefsFromStoreObj(storeObj, dataId) { | ||
let affected = false; | ||
|
||
const cleanedObj = mapValues(storeObj, (value, key) => { | ||
if (value === dataId) { | ||
affected = true; | ||
return null; | ||
} | ||
|
||
if (isArray(value)) { | ||
const filteredArray = cleanArray(value, dataId); | ||
|
||
if (filteredArray !== value) { | ||
affected = true; | ||
return filteredArray; | ||
} | ||
|
||
// If not modified, return the original value | ||
return value; | ||
} | ||
}); | ||
|
||
if (affected) { | ||
// Maintain === for unchanged objects | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I'm not misunderstanding this, we won't be able to maintain There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed. |
||
return cleanedObj; | ||
} else { | ||
return storeObj; | ||
} | ||
} | ||
|
||
// Remove any occurrences of dataId in an arbitrarily nested array, and make sure that the old array | ||
// === the new array if nothing was changed | ||
export function cleanArray(originalArray, dataId) { | ||
if (originalArray.length && isArray(originalArray[0])) { | ||
// Handle arbitrarily nested arrays | ||
let modified = false; | ||
const filteredArray = originalArray.map((nestedArray) => { | ||
const nestedFilteredArray = cleanArray(nestedArray, dataId); | ||
|
||
if (nestedFilteredArray !== nestedArray) { | ||
modified = true; | ||
return nestedFilteredArray; | ||
} | ||
|
||
return nestedArray; | ||
}); | ||
|
||
if (! modified) { | ||
return originalArray; | ||
} | ||
|
||
return filteredArray; | ||
} else { | ||
const filteredArray = originalArray.filter((item) => item !== dataId); | ||
|
||
if (filteredArray.length === originalArray.length) { | ||
// No items were removed, return original array | ||
return originalArray; | ||
} | ||
|
||
return filteredArray; | ||
} | ||
} | ||
|
||
// Reducer for 'ARRAY_DELETE' behavior | ||
function mutationResultArrayDeleteReducer(state: NormalizedCache, { | ||
behavior, | ||
}: MutationBehaviorReducerArgs): NormalizedCache { | ||
const { | ||
dataId, | ||
storePath, | ||
} = behavior as MutationArrayDeleteBehavior; | ||
|
||
const dataIdOfObj = storePath.shift(); | ||
const clonedObj = cloneDeep(state[dataIdOfObj]); | ||
const array = scopeJSONToResultPath({ | ||
json: clonedObj, | ||
path: storePath, | ||
}); | ||
|
||
array.splice(array.indexOf(dataId), 1); | ||
|
||
return assign(state, { | ||
[dataIdOfObj]: clonedObj, | ||
}) as NormalizedCache; | ||
} | ||
|
||
// Combines all of the default reducers into a map based on the behavior type they accept | ||
// The behavior type is used to pick the right reducer when evaluating the result of the mutation | ||
export const defaultMutationBehaviorReducers: { [type: string]: MutationBehaviorReducer } = { | ||
'ARRAY_INSERT': mutationResultArrayInsertReducer, | ||
'DELETE': mutationResultDeleteReducer, | ||
'ARRAY_DELETE': mutationResultArrayDeleteReducer, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should prefix with
APOLLO_
to be consistent no?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These aren't real actions - they end up in the
applyResults
section of theMUTATION_RESULT
action, here: https://github.com/apollostack/apollo-client/pull/320/files#diff-162241b86351f640a8bb3127ed5fbd5bR92This is because we want to process all of the mutation result stuff synchronously in one pass, rather than as multiple actions one after the other - it seems weird to me to fire many actions in one tick, when they represent one event (a mutation arriving from the server).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to not call these actions at all? I was confused at first by this as well. It seems that these are more like "action applications" although I don't think that is a good name for them. I just think calling them "actions" makes it seem as though they are actual actions which will lead to a transition within the Redux store which can be a bit confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm going to call them "behaviors" after thinking about it.