rescript-relay
Version:
Use Relay with ReScript.
425 lines (356 loc) • 12.5 kB
JavaScript
function getNewObj(maybeNewObj, currentObj) {
return maybeNewObj || Object.assign({}, currentObj);
}
function getPathName(path) {
return path.join("_");
}
function makeNewPath(currentPath, newKeys) {
return [].concat(currentPath, newKeys);
}
function getTypename(v) {
if (v != null && typeof v === "object" && v.__typename != null) {
return v.__typename;
}
}
function unwrapInputUnion(obj) {
if (obj != null && typeof obj === "object" && "__$inputUnion" in obj) {
return {
[obj["__$inputUnion"]]: obj["_0"],
};
}
return obj;
}
/**
* Runs on each object in the tree and follows the provided instructions
* to apply transforms etc.
*/
function traverse(
fullInstructionMap,
currentPath,
currentObj,
instructionMap,
converters,
nullableValue,
instructionPaths,
addFragmentOnRoot
) {
// We lazily set up a new object for each "level", as we don't want to mutate
// what comes back from the Relay store, and nor do we want to create new
// objects unless we need to. And we only need to when we need to change
// something.
var newObj;
if (addFragmentOnRoot) {
newObj = getNewObj(newObj, currentObj);
newObj.fragmentRefs = Object.assign({}, newObj);
newObj.updatableFragmentRefs = newObj.fragmentRefs;
}
for (var key in currentObj) {
// Ensure we don't move into internal properties coming from Relay, with the
// exception of the few names allowed to start with double underscores
// ("__typename" and "__id"), and any Relay provided variables.
if (
key.startsWith("__") &&
key !== "__typename" &&
key !== "__id" &&
!key.startsWith("__relay_internal")
)
continue;
var isUnion = false;
var originalValue = currentObj[key];
// Instructions are stored by the path in the object where they apply
var thisPath = makeNewPath(currentPath, [key]);
var path = getPathName(thisPath);
var instructions = instructionMap[path] || {};
if (currentObj[key] == null) {
newObj = getNewObj(newObj, currentObj);
newObj[key] = nullableValue;
continue;
}
if (currentObj[key] && currentObj[key].BS_PRIVATE_NESTED_SOME_NONE >= 0) {
newObj = getNewObj(newObj, currentObj);
newObj[key] = currentObj[key];
continue;
}
var shouldConvertRootObj =
typeof instructions["r"] === "string" &&
fullInstructionMap[instructions["r"]];
var shouldAddFragmentFn = instructions["f"] === "";
var shouldConvertEnum =
typeof instructions["e"] === "string" && !!converters[instructions["e"]];
// This is true for non-arrays that are to be converted
var shouldConvertCustomField =
typeof instructions["c"] === "string" && !!converters[instructions["c"]];
// This is true for arrays that are to be converted. This and the above
// won't be true at the same time.
var shouldConvertCustomFieldArray =
typeof instructions["ca"] === "string" &&
!!converters[instructions["ca"]];
// Special case when this is a custom field that's an array. Ensures we
// don't accidentally move into the array when we're not supposed to.
if (shouldConvertCustomFieldArray && Array.isArray(currentObj[key])) {
newObj = getNewObj(newObj, currentObj);
newObj[key] = currentObj[key].map(converters[instructions["ca"]]);
return newObj;
}
var shouldBlockTraversal = typeof instructions["b"] === "string";
var allowGoingIntoArray = shouldBlockTraversal
? instructions["b"] === "a"
: true;
if (shouldBlockTraversal && !allowGoingIntoArray) {
newObj = getNewObj(newObj, currentObj);
continue;
}
var shouldConvertUnion =
typeof instructions["u"] === "string" && !!converters[instructions["u"]];
/**
* Handle arrays
*/
// Special case when this is a custom field that's an array. Ensures we
// don't accidentally move into the array when we're not supposed to.
if (shouldConvertCustomField && Array.isArray(currentObj[key])) {
newObj = getNewObj(newObj, currentObj);
newObj[key] = converters[instructions["c"]](originalValue);
return newObj;
}
if (Array.isArray(currentObj[key])) {
newObj = getNewObj(newObj, currentObj);
newObj[key] = currentObj[key].map(function (v) {
if (v == null) {
return nullableValue;
}
if (shouldConvertRootObj) {
return traverser(
unwrapInputUnion(v),
fullInstructionMap,
converters,
nullableValue,
instructions["r"]
);
}
if (shouldConvertEnum) {
return converters[instructions["e"]](v);
}
if (shouldConvertCustomField) {
return converters[instructions["c"]](v);
}
if (shouldConvertUnion && v != null && typeof v === "object") {
var typename = getTypename(v);
if (typename != null) {
isUnion = true;
var unionObj = v;
// Means we're wrapping, and this will be a ReScript value.
if (nullableValue === null) {
// Convert it back to a flat JS value
unionObj = converters[instructions["u"]](v);
}
var newPath = makeNewPath(currentPath, [key, typename]);
var unionRootHasFragment =
(instructionMap[getPathName(newPath)] || {}).f === "";
var traversedValue = traverse(
fullInstructionMap,
newPath,
unionObj,
instructionMap,
converters,
nullableValue,
instructionPaths,
unionRootHasFragment
);
// Undefined means we're going from JS to ReScript, in which case we
// need to run the conversion here rather than earlier.
return nullableValue === undefined
? converters[instructions["u"]](traversedValue)
: traversedValue;
}
}
if (shouldAddFragmentFn && typeof v === "object" && !Array.isArray(v)) {
var objWithFragmentFn = Object.assign({}, v);
objWithFragmentFn.fragmentRefs = Object.assign({}, objWithFragmentFn);
objWithFragmentFn.updatableFragmentRefs =
objWithFragmentFn.fragmentRefs;
return objWithFragmentFn;
}
return v;
});
} else {
/**
* Handle normal values.
*/
var v = currentObj[key];
if (shouldConvertRootObj) {
newObj = getNewObj(newObj, currentObj);
newObj[key] = traverser(
unwrapInputUnion(v),
fullInstructionMap,
converters,
nullableValue,
instructions["r"]
);
continue;
}
if (shouldConvertEnum) {
newObj = getNewObj(newObj, currentObj);
newObj[key] = converters[instructions["e"]](v);
}
if (shouldConvertCustomField) {
newObj = getNewObj(newObj, currentObj);
newObj[key] = converters[instructions["c"]](v);
// Ensure that the custom scalar value itself isn't traversed more.
continue;
}
if (shouldConvertUnion && v != null && typeof v === "object") {
var typename = getTypename(v);
if (typename != null) {
isUnion = true;
var unionObj = v;
// Means we're wrapping, and this will be a ReScript value.
if (nullableValue === null) {
// Convert it back to a flat JS value
unionObj = converters[instructions["u"]](v);
}
var newPath = makeNewPath(currentPath, [key, typename]);
var unionRootHasFragment =
(instructionMap[getPathName(newPath)] || {}).f === "";
var traversedValue = traverse(
fullInstructionMap,
newPath,
unionObj,
instructionMap,
converters,
nullableValue,
instructionPaths,
unionRootHasFragment
);
newObj = getNewObj(newObj, currentObj);
newObj[key] =
// Undefined means we're going from JS to ReScript, in which case we
// need to run the conversion here rather than earlier.
nullableValue === undefined
? converters[instructions["u"]](traversedValue)
: traversedValue;
}
}
if (shouldAddFragmentFn && typeof v === "object" && !Array.isArray(v)) {
newObj = getNewObj(newObj, currentObj);
var objWithFragmentFn = Object.assign({}, v);
objWithFragmentFn.fragmentRefs = Object.assign({}, objWithFragmentFn);
objWithFragmentFn.updatableFragmentRefs =
objWithFragmentFn.fragmentRefs;
newObj[key] = objWithFragmentFn;
}
}
if (originalValue != null && !isUnion) {
var nextObj = (newObj && newObj[key]) || currentObj[key];
if (typeof nextObj === "object" && !Array.isArray(originalValue)) {
var traversedObj = traverse(
fullInstructionMap,
thisPath,
nextObj,
instructionMap,
converters,
nullableValue,
instructionPaths
);
if (traversedObj !== nextObj) {
newObj = getNewObj(newObj, currentObj);
newObj[key] = traversedObj;
}
} else if (Array.isArray(originalValue) && !shouldBlockTraversal) {
newObj = getNewObj(newObj, currentObj);
newObj[key] = nextObj.map(function (o) {
if (typeof o === "object" && o != null && !Array.isArray(o)) {
return traverse(
fullInstructionMap,
thisPath,
o,
instructionMap,
converters,
nullableValue,
instructionPaths
);
} else if (o == null) {
return nullableValue;
} else {
return o;
}
});
}
}
}
return newObj || currentObj;
}
/**
* This function takes an object (snapshot from the Relay store) and applies a
* set of conversions deeply on the object (instructions coming from "converters"-prop).
* It converts nullable values either to null or undefined, and it wraps/unwraps enums
* and unions.
*
* It preserves structural integrity where possible, and return new objects where properties
* have been modified.
*/
function traverser(
root,
instructionMaps_,
theConverters,
nullableValue,
rootObjectKey
) {
if (!root) {
return nullableValue;
}
var instructionMaps = instructionMaps_ || {};
var instructionMap = instructionMaps[rootObjectKey || "__root"] || {};
var converters = theConverters == null ? {} : theConverters;
var instructionPaths = Object.keys(instructionMap);
// We'll add the fragmentRefs reference to the root if needed here.
var fragmentsOnRoot = (instructionMap[""] || {}).f === "";
var unionRootConverter = converters[(instructionMap[""] || {}).u];
if (Array.isArray(root)) {
return root.map(function (v) {
if (v == null) {
return nullableValue;
}
var n = [];
// Since a root level union is treated as a "new root level", we'll need
// to do a separate check here of whether there's a fragment on the root
// we need to account for, or not.
if (unionRootConverter != null) {
n = [v.__typename];
fragmentsOnRoot = (instructionMap[v.__typename] || {}).f === "";
}
var traversedObj = traverse(
instructionMaps,
n,
v,
instructionMap,
converters,
nullableValue,
instructionPaths,
fragmentsOnRoot
);
return unionRootConverter != null
? unionRootConverter(traversedObj)
: traversedObj;
});
}
var newObj = Object.assign({}, root);
var n = [];
// Same as in the union array check above - if there's a fragment in the new
// root created by the union, we need to account for that separately here.
if (unionRootConverter != null) {
n = [newObj.__typename];
fragmentsOnRoot = (instructionMap[newObj.__typename] || {}).f === "";
}
var v = traverse(
instructionMaps,
n,
newObj,
instructionMap,
converters,
nullableValue,
instructionPaths,
fragmentsOnRoot
);
return unionRootConverter != null ? unionRootConverter(v) : v;
}
module.exports = { traverser };