You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

90 lines
4.9 KiB
JavaScript

"use strict";
const unreachable = require("@joepie91/unreachable")("zapdb");
const compose = require("../../util/compose");
const schemaRules = require("./schema-rules");
module.exports = function createTransformComputer() {
let automaticTransformers = [];
let requiredTransformers = [];
return {
changeType: function (oldType, newType) {
if (oldType == null || newType == null) {
// We're setting a type for this field for the first time (which should only happen during field creation). This also applies to the inverse computation for rollbacks
return;
} else {
let automatic = schemaRules.types[oldType].losslessConversionTo[newType];
if (automatic != null) {
automaticTransformers.push(automatic);
} else {
requiredTransformers.push({ type: "type", oldType, newType });
}
}
},
changeAttribute: function (attribute, oldValue, newValue) {
if (oldValue == null || newValue == null) {
// We're setting this attribute on this field for the first time (which should only happen during field creation). This also applies to inverse computation for rollbacks.
// NOTE: Even if a field is not required, it should always be initialized during field creation, using an implicit operation setting the default, so a legitimate revert to `undefined` should never be possible.
// FIXME: How to deal with this when a new attribute is introduced in a new schema DSL version? Should we just pretend that the old one always existed, by updating the old DSL implementation to insert it implicitly as a default? Probably should.
return;
} else {
let canBeAutomatic = schemaRules.attributes[attribute].isLossless(oldValue, newValue);
if (canBeAutomatic) {
automaticTransformers.push(schemaRules.attributes[attribute].losslessTransformer);
} else {
requiredTransformers.push({ type: "attribute", attribute, oldValue, newValue });
}
}
},
getResults: function (manualTransformer, isForward, fieldName) {
// NOTE: We disallow transformTo/rollbackTo when they are not required; if the user wishes to bulk-transform values, they should specify a changeRecords operation instead (which should probably require the user to specify both the forward and backward transform?). Otherwise, we cannot implement "maybe you forgot a rollbackTo" errors, because that's only actually an error when a transform isn't *required*, and so if a user 'overreaches' in their type transform to also do a value transform we can't detect missing corresponding rollbackTo logic.
// NOTE: There are deliberately duplicate conditional clauses in here to improve readability!
if (requiredTransformers.length === 0 && automaticTransformers.length === 0) {
if (manualTransformer == null) {
// Identity function; no changes were made that affect the value itself
return (value) => value;
} else {
// FIXME: Better error message
let operationName = (isForward) ? "transformTo" : "rollbackTo";
throw new Error(`A ${operationName} operation was specified, but no other schema changes require one. Maybe you meant to use updateRecords instead?`);
}
} else if (requiredTransformers.length === 0 && automaticTransformers.length > 0) {
return compose(automaticTransformers);
} else if (requiredTransformers.length > 0) {
if (manualTransformer == null) {
// FIXME: Have some sort of error templating abstracted out instead
let causes = requiredTransformers
.map((cause) => {
if (cause.type === "type") {
return ` - Field type changed from '${cause.oldType}' to '${cause.newType}'`;
} else if (cause.type === "attribute") {
let from = (isForward) ? cause.oldValue : cause.newValue;
let to = (isForward) ? cause.newValue : cause.oldValue;
return ` - Attribute '${cause.attribute}' changed from '${util.inspect(from)}' to '${util.inspect(to)}'`;
} else {
throw unreachable("Unrecognized cause type");
}
})
.join("\n");
let errorMessage = (isForward)
? `One or more schema changes for '${fieldName}' cannot be applied automatically, because existing data would lose precision. You need to specify a transformTo operation manually.`
: `One or more schema changes for '${fieldName}' cannot be applied automatically, because rolling back the migration would cause data to lose precision. You need to specify a rollbackTo operation manually.`
// FIXME: Better error message
throw new Error(`${errorMessage}\n\nCaused by:\n${causes}`);
} else {
// TODO: Is this the correct order, always letting the manual transformer act on the original value rather than the one post automatic transforms? Probably is, if we want automatic transforms to be transparent to the user (and not produce a leaky abstraction)
return compose([ manualTransformer, ... automaticTransformers ]);
}
} else {
throw unreachable("Impossible condition");
}
}
};
};