@windingtree/wt-write-api
Version:
API to write data to the Winding Tree platform
299 lines (276 loc) • 9.03 kB
JavaScript
const _ = require('lodash');
const Web3Utils = require('web3-utils');
const tv4 = require('tv4');
const tv4Formats = require('tv4-formats');
const countryCodes = require('iso-3166-1-alpha-2');
const languageCodes = require('iso-639-1');
const currencyCodes = require('currency-codes');
const timezones = require('timezones.json');
const validator = require('validator');
const PhoneNumber = require('awesome-phonenumber');
const { errors: wtJsLibsErrors } = require('@windingtree/wt-js-libs');
const descriptionSchema = require('./description-schema.json');
const ratePlansSchema = require('./rateplans-schema.json');
const availabilitySchema = require('./availability-schema.json');
const orgNameSchema = require('./orgname-schema.json');
const contactsSchema = require('./contact-schema.json');
const addressSchema = require('./address-schema.json');
const uploadersSchema = require('./uploaders-schema.json');
const walletSchema = require('./wallet-schema.json');
const { wtLibs, allowedUploaders } = require('../../config');
class ValidationError extends Error {};
const TIMEZONES = new Set(_(timezones).map('utc').flatten().value());
tv4.addFormat(tv4Formats); // We use the "date-time" and "uri" formats from this module.
tv4.addFormat('country-code', (data) => {
if (countryCodes.getCountry(data)) {
return null;
}
return 'Not a valid ISO 3166-1 alpha-2 country code.';
});
tv4.addFormat('timezone', (data) => {
if (TIMEZONES.has(data)) {
return null;
}
return 'Not a valid timezone.';
});
tv4.addFormat('language-code', (data) => {
if (languageCodes.validate(data)) {
return null;
}
return 'Not a valid ISO 639-1 language code.';
});
tv4.addFormat('currency-code', (data) => {
if (currencyCodes.code(data)) {
return null;
}
return 'Not a valid ISO 4217 currency code.';
});
tv4.addFormat('email', (data) => {
if (validator.isEmail(data)) {
return null;
}
return 'Not a valid e-mail.';
});
tv4.addFormat('phone', (data) => {
const pn = new PhoneNumber(data);
if (pn.isValid()) {
return null;
}
return `Invalid phone number: ${data}`;
});
tv4.setErrorReporter((error, data, schema) => {
// Better error messages for some common error cases.
if (schema === uploadersSchema.definitions.uploader &&
error.code === tv4.errorCodes.ONE_OF_MISSING) {
return 'Invalid uploader configuration';
}
if (schema === uploadersSchema.definitions.uploader &&
error.code === tv4.errorCodes.ONE_OF_MULTIPLE) {
return 'Only one uploader can be configured per document';
}
});
/* Note: the json schemas were generated from the swagger
* definition at
*
* https://github.com/windingtree/wiki/blob/master/hotel-data-swagger.yaml
*
* (version 0.0.4) using the "openapi2schema" CLI tool.
*/
function _validate (data, schema) {
if (!tv4.validate(data, schema, false, true)) {
var msg = tv4.error.message + ': ' + tv4.error.dataPath;
throw new ValidationError(msg);
}
}
/**
* JSON schema can't validate uniqueness based on selected fields, we need to do custom validation.
*
* @param data
* @param uniqueFields Fields that are supposed to make the object key (be unique together).
* @params dataType Human readable data specification.
*/
function _checkDuplicities (data, uniqueFields, dataType) {
if (data) {
const keys = [];
for (const item of data) {
const key = [];
for (const field of uniqueFields) {
key.push(item[field]);
}
if (keys.find((k) => { return k.join(':') === key.join(':'); })) {
throw new ValidationError(`Duplicit value for ${dataType}: ${key}`);
}
keys.push(key);
}
}
}
/**
* Validate data against description json schema definition.
*
* @param {Object} data
* @return {undefined}
* @throws {ValidationError} When data validation fails.
*/
module.exports.validateDescription = function (data) {
_validate(data, descriptionSchema);
_checkDuplicities(data.roomTypes, ['id'], 'room type');
};
/**
* Validate data against organization name json schema definition.
*
* @param {Object} data
* @return {undefined}
* @throws {ValidationError} When data validation fails.
*/
module.exports.validateOrgName = function (data) {
_validate(data, orgNameSchema);
};
/**
* Validate data against contacts json schema definition.
*
* @param {Object} data
* @return {undefined}
* @throws {ValidationError} When data validation fails.
*/
module.exports.validateContacts = function (data) {
_validate(data, contactsSchema);
};
/**
* Validate data against address json schema definition.
*
* @param {Object} data
* @return {undefined}
* @throws {ValidationError} When data validation fails.
*/
module.exports.validateAddress = function (data) {
_validate(data, addressSchema);
};
/**
* Validate data against rate plans json schema definition.
*
* @param {Object} data
* @return {undefined}
* @throws {ValidationError} When data validation fails.
*/
module.exports.validateRatePlans = function (data) {
_validate(data, ratePlansSchema);
_checkDuplicities(data, ['id'], 'rate plan');
};
/**
* Validate data against availability json schema definition.
*
* @param {Object} data
* @return {undefined}
* @throws {ValidationError} When data validation fails.
*/
module.exports.validateAvailability = function (data) {
_validate(data, availabilitySchema);
_checkDuplicities(data.roomTypes, ['roomTypeId', 'date'], 'availability');
};
/**
* Validate the dataUri.
*
* @param {Object} data
* @return {undefined}
* @throws {ValidationError} When data validation fails.
*/
module.exports.validateDataUri = function (data) {
// TODO expose this list in wt-js-libs
const allowedProtocols = (Object.keys(wtLibs.options.offChainDataOptions.adapters));
const matchResult = data.match(/([a-zA-Z-]+):\/\//i);
const protocol = matchResult ? matchResult[1] : null;
if (!protocol || allowedProtocols.indexOf(protocol) === -1) {
throw new ValidationError(`Not a valid URL: ${data}`);
}
};
/**
* Validate the notifications url.
*
* @param {Object} data
* @return {undefined}
* @throws {ValidationError} When data validation fails.
*/
module.exports.validateNotifications = function (data) {
// eslint-disable-next-line camelcase
const opts = { require_protocol: true, require_tld: false };
if (data && !validator.isURL(data, opts)) {
throw new ValidationError(`Not a valid URL: ${data}`);
}
};
/**
* Validate the booking url.
*
* @param {Object} data
* @return {undefined}
* @throws {ValidationError} When data validation fails.
*/
module.exports.validateBooking = function (data) {
// eslint-disable-next-line camelcase
const opts = { protocols: ['https'], require_protocol: true, require_tld: false };
if (data && !validator.isURL(data, opts)) {
throw new ValidationError(`Not a valid secure URL: ${data}`);
}
};
/**
* Validate data using the web3 library.
*
* @param {Object} data
* @return {undefined}
* @throws {ValidationError} When data validation fails.
*/
module.exports.validateWallet = function (data) {
if (!(data instanceof Object)) { // This case is not handled by the "unlock" method.
throw new ValidationError('Not a valid V3 wallet');
}
_validate(data, walletSchema);
try {
const wallet = wtLibs.createWallet(data);
wallet.unlock('dummy');
} catch (err) {
if (err instanceof wtJsLibsErrors.MalformedWalletError) {
throw new ValidationError(err.message);
}
}
};
/**
* Validates the guarantee using the wt-js-libs by comparing
* claim and signature to a guarantor field in the claim
* @param {Object} data
* @return {undefined}
* @throws {ValidationError} When data validation fails.
*/
module.exports.validateGuarantee = function (guarantee) {
const client = wtLibs.getTrustClueClient();
try {
const decodedData = JSON.parse(Web3Utils.hexToUtf8(guarantee.claim));
client.verifySignedData(guarantee.claim, guarantee.signature, (actualSigner) => {
if (Web3Utils.toChecksumAddress(decodedData.guarantor) !== actualSigner) {
throw new Error(`Expected signer '${decodedData.guarantor}' does not match the recovered one '${actualSigner}'`);
}
}
);
} catch (err) {
throw new ValidationError(err.message);
}
};
// Check if all allowed uploaders are defined in the schema and
// patch the uploaders schema to reflect allowed uploaders.
for (const key of allowedUploaders) {
if (!uploadersSchema.definitions[key]) {
throw new Error(`Unknown uploader in 'allowedUploaders': ${key}`);
}
}
uploadersSchema.definitions.uploader.oneOf = allowedUploaders.map((uploader) => {
return { $ref: `#/definitions/${uploader}` };
});
/**
* Validate uploaders against their json schema definition.
*
* @param {Object} data
* @return {undefined}
* @throws {ValidationError} When data validation fails.
*/
module.exports.validateUploaders = function (data) {
return _validate(data, uploadersSchema);
};
module.exports.ValidationError = ValidationError;