gpii-universal
Version:
Cross platform, core components of the GPII personalization infrastructure.
229 lines (204 loc) • 9.47 kB
JavaScript
/*
kettle.middleware "validators" designed to validate preference and settings payloads.
*/
/* eslint-env node */
"use strict";
var fluid = require("infusion");
var gpii = fluid.registerNamespace("gpii");
// Add the ability to require JSON5 files, the default usage of these grades will need to have this or they will fail to
// expand the base schemas.
require("json5/lib/register");
fluid.registerNamespace("gpii.universal.solutionsRegistry.validators");
fluid.require("%gpii-json-schema");
require("../../../transformer/src/js/Transformer");
require("../../../settingsHandlers/");
require("../../../flowManager/src/SystemUtils");
/**
*
* A replacement for the default kettle validation middleware handle invokers backing function, which is more suitable
* for working with a schema that is generated based on the payload.
*
* @param {gpii.universal.solutionsRegistry.validators.base} that - The validation middleware component.
* @param {gpii.schema.validator} globalValidator - The persistent global validator component.
* @param {kettle.request.http} requestHandler - The component fielding the individual request.
* @return {Promise} - A `fluid.promise` that will resolve if the payload is valid or reject with errors if the payload is invalid.
*
*/
gpii.universal.solutionsRegistry.validators.validateRequest = function (that, globalValidator, requestHandler) {
var transformationRules = that.options.rules.outerPayload;
var outerPayload = fluid.model.transformWithRules(requestHandler.req, transformationRules);
var validationStages = [];
// "gross" validation based on the overall structure
validationStages.push(gpii.universal.solutionsRegistry.validators.singleValidation(that, globalValidator, that.options.baseSchema, outerPayload, that.options.baseSchemaHash));
// "deep" validation of individual settings, per solution.
var innerPayload = fluid.model.transformWithRules(requestHandler.req, that.options.rules.innerPayload);
fluid.each(innerPayload, function (solutionSettings, settingURI) {
var solutionID = gpii.universal.solutionsRegistry.validators.solutionIdFromURI(settingURI);
fluid.each(fluid.get(that.options.sr, [solutionID, "settingsHandlers"]), function (settingsHandler) {
var transformed = gpii.transformer.transformOneSettingsHandler(fluid.copy(settingsHandler), { settings: innerPayload }, solutionID);
if (Object.keys(transformed.settings).length) {
innerPayload[settingURI] = transformed.settings;
}
});
});
fluid.each(innerPayload, function (solutionSettings, settingURI) {
var solutionSchema = that.options.codex[settingURI];
if (solutionSchema) {
if (!that.schemaHashesByUri[settingURI]) {
that.schemaHashesByUri[settingURI] = gpii.schema.hashSchema(solutionSchema);
}
validationStages.push(gpii.universal.solutionsRegistry.validators.singleValidation(that, globalValidator, solutionSchema, solutionSettings, that.schemaHashesByUri[settingURI], [settingURI]));
}
});
// Although our validation is synchronous, we use this pattern so that processing will be cleanly interrupted on the
// first validation error.
return fluid.promise.sequence(validationStages);
};
/**
*
* Extract the solution ID (such as `com.microsoft.windows.desktopBackground`) from a solution URI, such as
* `http://registry.gpii.net/applications/com.microsoft.windows.desktopBackground`.
*
* @param {String} solutionURI - The solution URI.
* @return {String} - The solution ID.
*
*/
gpii.universal.solutionsRegistry.validators.solutionIdFromURI = function (solutionURI) {
var segments = solutionURI.split("/");
return segments[segments.length - 1];
};
/**
*
* Return a promise-returning function that represents a single attempt at validating a given payload. Meant to be used
* in a multi-pass validation approach with `fluid.promise.sequence`.
*
* @param {gpii.universal.solutionsRegistry.validators.base} validationMiddleware - The validation middleware component.
* @param {gpii.schema.validator} globalValidator - The persistent global validator component.
* @param {GssSchema} schema - A GSS schema.
* @param {Any} toValidate - The payload to validate.
* @param {String} schemaHash - A hash of the schema to use in
* @param {Array<String>} [dataPathPrefix] - A path to prepend to the `dataPath` reported in any validation errors.
* @return {Function} - A `fluid.promise` returning function.
*
*/
gpii.universal.solutionsRegistry.validators.singleValidation = function (validationMiddleware, globalValidator, schema, toValidate, schemaHash, dataPathPrefix) {
return function () {
var validationPromise = fluid.promise();
var validationResults = globalValidator.validate(schema, toValidate, schemaHash);
if (validationResults.isValid) {
validationPromise.resolve();
}
else {
// TODO: We have to reconstruct the original path somehow
if (dataPathPrefix) {
fluid.each(validationResults.errors, function (singleError) {
singleError.dataPath = fluid.copy(dataPathPrefix).concat(singleError.dataPath);
});
}
var localisedErrors = gpii.schema.validator.localiseErrors(validationResults.errors, toValidate, validationMiddleware.model.messages, validationMiddleware.options.rules.localisation);
var localisedPayload = fluid.copy(validationResults);
localisedPayload.errors = localisedErrors;
var failurePayload = fluid.extend({}, validationMiddleware.options.errorTemplate, localisedPayload);
validationPromise.reject(failurePayload);
}
return validationPromise;
};
};
fluid.defaults("gpii.universal.solutionsRegistry.validators.base", {
gradeNames: ["kettle.middleware", "fluid.modelComponent"],
members: {
schemaHashesByUri: {}
},
model: {
messages: gpii.schema.messages.validationErrors
},
errorTemplate: {
// "Bad Request": https://developer.mozilla.org/nl/docs/Web/HTTP/Status/400
statusCode: 400,
message: "Your request was invalid. See the errors for details."
},
mergePolicy: {
"rules.localisation": "nomerge",
"rules.innerPayload": "nomerge",
"rules.outerPayload": "nomerge",
"rules.transformedPayload": "nomerge"
},
pathToSettings: "",
codexPath: "%gpii-universal/build/schemas/solution-schema-codex.json",
codex: "@expand:fluid.require({that}.options.codexPath)",
// TODO: Discuss combining the "codex" and "solutions registry" in the upcoming LSR work.
srPath: "%gpii-universal/testData/solutions/win32.json5",
sr: "@expand:fluid.require({that}.options.srPath)",
baseSchemaPath: "%gpii-universal/gpii/node_modules/solutionsRegistry/src/schemas/settings-base-schema.json5",
baseSchema: "@expand:fluid.require({that}.options.baseSchemaPath)",
baseSchemaHash: "@expand:gpii.schema.hashSchema({that}.options.requestSchema)",
rules: {
localisation: {
"": ""
},
outerPayload: {
"": "body"
},
innerPayload: {
"": ""
},
transformedPayload: {
"": "toValidate",
flat: {
contexts: {
"gpii-default": {
"preferences": "transformedSettings"
}
}
}
}
},
invokers: {
handle: {
funcName: "gpii.universal.solutionsRegistry.validators.validateRequest",
args: ["{that}", "{gpii.schema.validator}", "{arguments}.0"] // kettleValidator, globalValidator, request
}
}
});
// TODO: discuss how to handle contexts other than gpii-default in both of these stacks.
fluid.defaults("gpii.universal.solutionsRegistry.validators.settings", {
gradeNames: ["gpii.universal.solutionsRegistry.validators.base"],
rules: {
innerPayload: {
"": "body.flat.contexts.gpii-default.preferences"
}
}
});
fluid.defaults("gpii.universal.solutionsRegistry.requestHandlers.settings", {
gradeNames: ["kettle.request.http"],
requestMiddleware: {
validate: {
middleware: "{gpii.universal.solutionsRegistry.validators.settings}"
}
}
});
fluid.defaults("gpii.universal.solutionsRegistry.validators.preferences", {
gradeNames: ["gpii.universal.solutionsRegistry.validators.base"],
baseSchemaPath: "%gpii-universal/gpii/node_modules/solutionsRegistry/src/schemas/prefs-base-schema.json5",
rules: {
innerPayload: {
"": {
transform: {
type: "fluid.transforms.firstValue",
values: [
"body.contexts.gpii-default.preferences.application",
"body.contexts.gpii-default.preferences"
]
}
}
}
}
});
fluid.defaults("gpii.universal.solutionsRegistry.requestHandlers.preferences", {
gradeNames: ["kettle.request.http"],
requestMiddleware: {
validate: {
middleware: "{gpii.universal.solutionsRegistry.validators.preferences}"
}
}
});