@windingtree/wt-read-api
Version:
API to interact with the Winding Tree platform
258 lines (249 loc) • 10.1 kB
JavaScript
const _ = require('lodash');
const { errors: wtJsLibsErrors } = require('@windingtree/wt-js-libs');
const wtJsLibs = require('../services/wt-js-libs');
const { flattenObject, formatError } = require('../services/utils');
const { config } = require('../config');
const { DataFormatValidator } = require('../services/validation');
const {
HttpValidationError,
Http404Error,
HttpBadGatewayError,
} = require('../errors');
const {
calculateHotelsFields,
calculateHotelFields,
} = require('../services/fields');
const {
DEFAULT_PAGE_SIZE,
SCHEMA_PATH,
HOTEL_SCHEMA_MODEL,
VALIDATION_WARNING_HEADER,
} = require('../constants');
const {
mapHotelObjectToResponse,
REVERSED_HOTEL_FIELD_MAPPING,
} = require('../services/property-mapping');
const {
paginate,
LimitValidationError,
MissingStartWithError,
} = require('../services/pagination');
const resolveHotelObject = async (hotel, offChainFields, onChainFields) => {
let hotelData = {};
try {
if (offChainFields.length) {
const hotelApis = await hotel.getWindingTreeApi();
const apiContents = (await hotelApis.hotel[0].toPlainObject(offChainFields)).contents;
const flattenedOffChainData = flattenObject(apiContents, offChainFields);
hotelData = {
dataFormatVersion: apiContents.dataFormatVersion,
...flattenedOffChainData.descriptionUri,
};
// Some offChainFields need special treatment
const fieldModifiers = {
defaultLocale: (data, source, key) => { data[key] = source[key]; return data; },
guarantee: (data, source, key) => { data[key] = source[key]; return data; },
notificationsUri: (data, source, key) => { data[key] = source[key]; return data; },
bookingUri: (data, source, key) => { data[key] = source[key]; return data; },
ratePlansUri: (data, source, key) => { data.ratePlans = source[key]; return data; },
availabilityUri: (data, source, key) => { data.availability = source[key]; return data; },
};
for (const fieldModifier in fieldModifiers) {
if (flattenedOffChainData[fieldModifier] !== undefined) {
hotelData = fieldModifiers[fieldModifier](hotelData, flattenedOffChainData, fieldModifier);
}
}
}
for (let i = 0; i < onChainFields.length; i += 1) {
if (hotel[onChainFields[i]]) {
hotelData[onChainFields[i]] = await hotel[onChainFields[i]];
}
}
// Always append hotel chain address as id property
hotelData.id = hotel.address;
} catch (e) {
let message = 'Cannot get hotel data';
if (e instanceof wtJsLibsErrors.RemoteDataReadError) {
message = 'Cannot access on-chain data, maybe the deployed smart contract is broken';
}
if (e instanceof wtJsLibsErrors.StoragePointerError) {
message = 'Cannot access off-chain data';
}
return {
error: message,
originalError: e.message,
data: {
id: hotel.address,
},
};
}
return mapHotelObjectToResponse(hotelData);
};
const fillHotelList = async (path, fields, hotels, limit, startWith) => {
limit = limit ? parseInt(limit, 10) : DEFAULT_PAGE_SIZE;
const { items, nextStart } = paginate(hotels, limit, startWith, 'address');
let realItems = [], warningItems = [], realErrors = [];
const swaggerDocument = await DataFormatValidator.loadSchemaFromPath(SCHEMA_PATH, HOTEL_SCHEMA_MODEL, fields.mapped, REVERSED_HOTEL_FIELD_MAPPING);
const promises = [];
for (let hotel of items) {
if (config.checkOrgJsonHash && !await hotel.validateOrgJsonHash()) {
realErrors.push({
error: 'ORG.JSON hash validation failed',
data: { id: hotel.address },
});
continue;
}
promises.push((() => {
let resolvedHotelObject;
return resolveHotelObject(hotel, fields.toFlatten, fields.onChain)
.then(async (resolved) => {
resolvedHotelObject = resolved;
if (resolvedHotelObject.error) {
throw new HttpValidationError(resolvedHotelObject.error);
}
const passesTrustworthinessTest = await wtJsLibs.passesTrustworthinessTest(resolvedHotelObject.id, resolvedHotelObject.guarantee);
// silently remove all that does not pass the test
if (!passesTrustworthinessTest) {
return;
}
DataFormatValidator.validate(
resolvedHotelObject,
HOTEL_SCHEMA_MODEL,
swaggerDocument.components.schemas,
config.dataFormatVersions.hotels,
undefined,
'hotel',
fields.mapped
);
realItems.push(_.omit(resolvedHotelObject, fields.toDrop));
}).catch((e) => {
if (e instanceof HttpValidationError) {
hotel = {
error: 'Upstream hotel data format validation failed: ' + e.toString(),
originalError: e.data && e.data.errors && e.data.errors.length && e.data.errors.map((err) => { return err.toString(); }).join(';'),
data: resolvedHotelObject,
};
if (e.data && e.data.valid) {
warningItems.push(hotel);
} else {
hotel.data = e.data && e.data.data;
realErrors.push(hotel);
}
} else {
throw e;
}
});
})());
}
await Promise.all(promises);
const clientFields = _.xor(fields.mapped, fields.toDrop).join(',');
let next = nextStart ? `${config.baseUrl}${path}?limit=${limit}&fields=${clientFields}&startWith=${nextStart}` : undefined;
if (realErrors.length && realItems.length < limit && nextStart) {
const nestedResult = await fillHotelList(path, fields, hotels, limit - realItems.length, nextStart);
realItems = realItems.concat(nestedResult.items);
warningItems = warningItems.concat(nestedResult.warnings);
realErrors = realErrors.concat(nestedResult.errors);
if (realItems.length && nestedResult.nextStart) {
next = `${config.baseUrl}${path}?limit=${limit}&fields=${clientFields}&startWith=${nestedResult.nextStart}`;
} else {
next = undefined;
}
}
return {
items: realItems,
warnings: warningItems,
errors: realErrors,
next,
nextStart,
};
};
// Actual controllers
const findAll = async (req, res, next) => {
const { limit, startWith, fields } = req.query;
try {
const hotels = await res.locals.wt.hotelDirectory.getOrganizations();
const { items, warnings, errors, next } = await fillHotelList(req.path, calculateHotelsFields(fields), hotels, limit, startWith);
res.status(200).json({ items, warnings, errors, next });
} catch (e) {
if (e instanceof LimitValidationError) {
return next(new HttpValidationError('paginationLimitError', 'Limit must be a natural number greater than 0.'));
}
if (e instanceof MissingStartWithError) {
return next(new Http404Error('paginationStartWithError', 'Cannot find startWith in hotel collection.'));
}
next(e);
}
};
const find = async (req, res, next) => {
try {
const fields = calculateHotelFields(req.query.fields);
const swaggerDocument = await DataFormatValidator.loadSchemaFromPath(SCHEMA_PATH, HOTEL_SCHEMA_MODEL, fields.mapped, REVERSED_HOTEL_FIELD_MAPPING);
let resolvedHotel;
try {
resolvedHotel = await resolveHotelObject(res.locals.wt.hotel, fields.toFlatten, fields.onChain);
if (resolvedHotel.error) {
return next(new HttpBadGatewayError('hotelNotAccessible', resolvedHotel.error, 'Hotel data is not accessible.'));
}
const passesTrustworthinessTest = await wtJsLibs.passesTrustworthinessTest(resolvedHotel.id, resolvedHotel.guarantee);
// If a hotel does not pass the test, it's like it never existed
if (!passesTrustworthinessTest) {
return next(new Http404Error('hotelNotFound', 'Hotel does not pass the trustworthiness test.', 'Hotel not found'));
}
DataFormatValidator.validate(
resolvedHotel,
HOTEL_SCHEMA_MODEL,
swaggerDocument.components.schemas,
config.dataFormatVersions.hotels,
undefined,
'hotel',
fields.mapped
);
resolvedHotel = _.omit(resolvedHotel, fields.toDrop);
} catch (e) {
if (e instanceof HttpValidationError) {
const err = formatError(e);
err.data = resolvedHotel;
if (e.data && e.data.valid) {
return res.set(VALIDATION_WARNING_HEADER, e.data.errors).status(200).json(err.toPlainObject());
} else {
return res.status(err.status).json(err.toPlainObject());
}
} else {
next(e);
}
}
return res.status(200).json(resolvedHotel);
} catch (e) {
// improve error handling
return next(new HttpBadGatewayError('hotelNotAccessible', e.message, 'Hotel data is not accessible.'));
}
};
const meta = async (req, res, next) => {
try {
const hotelApis = await res.locals.wt.hotel.getWindingTreeApi();
const apiObject = await hotelApis.hotel[0].toPlainObject([]);
const passesTrustworthinessTest = await wtJsLibs.passesTrustworthinessTest(res.locals.wt.hotel.address, apiObject.contents.guarantee);
if (!passesTrustworthinessTest) {
return next(new Http404Error('hotelNotFound', 'Hotel does not pass the trustworthiness test.', 'Hotel not found'));
}
return res.status(200).json({
address: res.locals.wt.hotel.address,
dataIndexUri: apiObject.ref,
orgJsonUri: await res.locals.wt.hotel.orgJsonUri,
orgJsonHash: await res.locals.wt.hotel.orgJsonHash,
descriptionUri: apiObject.contents.descriptionUri,
ratePlansUri: apiObject.contents.ratePlansUri,
availabilityUri: apiObject.contents.availabilityUri,
dataFormatVersion: apiObject.contents.dataFormatVersion,
defaultLocale: apiObject.contents.defaultLocale,
guarantee: apiObject.contents.guarantee,
});
} catch (e) {
return next(new HttpBadGatewayError('hotelNotAccessible', e.message, 'Hotel data is not accessible.'));
}
};
module.exports = {
find,
findAll,
meta,
};