Created
November 4, 2017 16:21
-
-
Save ssured/e1b89a2271dd783bcd2002615607d35f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { types as t, flow, getEnv, getType } from 'mobx-state-tree'; | |
it('eagerly loads referenced objects', async () => { | |
expect.assertions(1); | |
const Author = t.model('Author', { | |
id: t.identifier(t.string), | |
name: t.string, | |
}); | |
const Post = t.model('Post', { | |
id: t.identifier(t.string), | |
author: t.reference(Author), | |
text: t.string, | |
}); | |
const Store = createAPIStore('Store', { | |
authors: { | |
type: Author, | |
fetch: (self, id) => getEnv(self).api.fetchAuthor(id), | |
}, | |
posts: { | |
type: Post, | |
fetch: (self, id) => getEnv(self).api.fetchPost(id), | |
}, | |
}); | |
/* | |
Store now has the following signature: | |
authors: t.optional(t.map(Author), {}), | |
posts: t.optional(t.map(Post), {}), | |
and actions: | |
fetchFromApi(type, id) => return promise to MST node of type `type` | |
fetchAuthor(id) => return promise to MST node of type `Author` | |
fetchPost(id) => return promise to MST node of type `Post` | |
you can enhance the store using `.props`, `.views`, `.actions` or `.extend` | |
*/ | |
// provide the actual api in the env, so it's easily testable | |
const store = Store.create( | |
{}, | |
{ | |
api: { | |
fetchPost: async id => await data().posts[id], | |
fetchAuthor: async id => await data().authors[id], | |
}, | |
} | |
); | |
// fetch an object, after the promise resolves, all references are in the MST | |
const post = await store.fetchPost('post1'); | |
expect(post.author.name).toEqual('Isaac Newton'); | |
}); | |
/* | |
createAPIStore | |
returns an (opionated) MST root object based on an api description | |
references to all types in the api description are automatically | |
fetched by the provided api callback functions | |
*/ | |
function createAPIStore(name, apiDescription) { | |
return ( | |
t | |
// create the t.model with `t.optional(t.map(<type>), {})` for each | |
// provided type in the apiDescription | |
.model( | |
name, | |
Object.keys(apiDescription).reduce((props, propertyName) => { | |
const { type } = apiDescription[propertyName]; | |
props[propertyName] = t.optional(t.map(type), {}); | |
return props; | |
}, {}) | |
) | |
// add actions for fetching data for each provided type, using the | |
// `fetch` functions in the apiDescription | |
.actions(self => { | |
function getReferenceProperties(type) { | |
// fetches all direct descendant t.reference properties from a type | |
// does not respect decorators like t.maybe and t.refinement | |
// returns list of [propertyKey, referenceType] | |
const { properties } = type; | |
return Object.keys(properties) | |
.filter(key => properties[key].name.match(/reference\((\w+)\)/)) | |
.reduce( | |
(map, key) => map.concat([[key, properties[key].targetType]]), | |
[] | |
); | |
} | |
function parseReferenceFromError(e) { | |
// parses reference Id from error | |
const resolveReferenceErrorRexeg = /Failed to resolve reference of type \w+: '(\w+)' \(in:/; | |
return (e.message.match(resolveReferenceErrorRexeg) || [])[1]; | |
} | |
// transform the description to be able to use type.name as a key | |
const apiMap = Object.keys( | |
apiDescription | |
).reduce((map, propertyName) => { | |
const { type, fetch } = apiDescription[propertyName]; | |
map[type.name] = { type, propertyName, fetch }; | |
return map; | |
}, {}); | |
return { | |
...Object.keys(apiMap).reduce((fetches, typeName) => { | |
fetches[`fetch${typeName}`] = id => | |
self.fetchFromApi(apiMap[typeName].type, id); | |
return fetches; | |
}, {}), | |
fetchFromApi: flow(function* fetchFromApi(type, id) { | |
const { propertyName, fetch } = apiMap[type.name]; | |
// short circuit if we already have the data | |
if (self[propertyName].has(id)) { | |
return self[propertyName].get(id); | |
} | |
// assign with a POJO | |
self[propertyName].set(id, yield fetch(self, id)); | |
// get back an MST node | |
const node = self[propertyName].get(id); | |
// wait for loading all referenced objects | |
yield Promise.all( | |
getReferenceProperties(getType(node)) | |
// find all reference errors | |
.map(([key, type]) => { | |
try { | |
node[key]; // just access the property, if it's an error we'll catch it | |
} catch (e) { | |
return [type, parseReferenceFromError(e)]; | |
} | |
}) | |
// filter all empty references | |
.filter(t => !!t && !!t[1]) | |
// fetch the references | |
.map(([type, reference]) => self.fetchFromApi(type, reference)) | |
); | |
return node; | |
}), | |
}; | |
}) | |
); | |
} | |
// put data in a function so it can be moved to the end of the script | |
function data() { | |
return { | |
authors: { | |
isaac: { | |
id: 'isaac', | |
name: 'Isaac Newton', | |
}, | |
albert: { | |
id: 'albert', | |
name: 'Albert Einstein', | |
}, | |
}, | |
posts: { | |
post1: { | |
id: 'post1', | |
author: 'isaac', | |
text: 'Apples fall due to gravity!', | |
}, | |
post2: { | |
id: 'post2', | |
author: 'albert', | |
text: 'Lightspeed is constant!', | |
}, | |
post3: { | |
id: 'post3', | |
author: 'albert', | |
text: 'Gravity bends spacetime!', | |
}, | |
}, | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment