Skip to content

Instantly share code, notes, and snippets.

@ssured
Created November 4, 2017 16:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ssured/e1b89a2271dd783bcd2002615607d35f to your computer and use it in GitHub Desktop.
Save ssured/e1b89a2271dd783bcd2002615607d35f to your computer and use it in GitHub Desktop.
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