@windingtree/wt-read-api
Version:
API to interact with the Winding Tree platform
248 lines (236 loc) • 9.12 kB
JavaScript
const _ = require('lodash');
const { errors: wtJsLibsErrors } = require('@windingtree/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 {
calculateAirlineFields,
calculateAirlinesFields,
} = require('../services/fields');
const {
DEFAULT_PAGE_SIZE,
SCHEMA_PATH,
AIRLINE_SCHEMA_MODEL,
VALIDATION_WARNING_HEADER,
} = require('../constants');
const {
mapAirlineObjectToResponse,
REVERSED_AIRLINE_FIELD_MAPPING,
} = require('../services/property-mapping');
const {
paginate,
LimitValidationError,
MissingStartWithError,
} = require('../services/pagination');
const resolveAirlineObject = async (airline, offChainFields, onChainFields) => {
let airlineData = {};
try {
if (offChainFields.length) {
const loadInstances = offChainFields.indexOf('flightsUri.items.flightInstancesUri') > -1;
const depth = loadInstances ? undefined : 3;
const airlineApis = await airline.getWindingTreeApi();
const apiContents = (await airlineApis.airline[0].toPlainObject(offChainFields, depth)).contents;
if (!loadInstances && apiContents.flightsUri && apiContents.flightsUri.contents) {
for (const flight of apiContents.flightsUri.contents.items) {
delete flight.flightInstancesUri;
}
}
const flattenedOffChainData = flattenObject(apiContents, offChainFields);
airlineData = {
dataFormatVersion: apiContents.dataFormatVersion,
...flattenedOffChainData.descriptionUri,
};
// Some offChainFields need special treatment
const fieldModifiers = {
notificationsUri: (data, source, key) => { data[key] = source[key]; return data; },
bookingUri: (data, source, key) => { data[key] = source[key]; return data; },
flightsUri: (data, source, key) => {
data.flights = source[key];
if (data.flights) {
for (const f of data.flights.items) {
if (f.flightInstancesUri && f.flightInstancesUri.ref && f.flightInstancesUri.contents) {
f.flightInstances = f.flightInstancesUri.contents;
} else {
f.flightInstances = f.flightInstancesUri;
}
delete f.flightInstancesUri;
}
}
return data;
},
};
for (const fieldModifier in fieldModifiers) {
if (flattenedOffChainData[fieldModifier] !== undefined) {
airlineData = fieldModifiers[fieldModifier](airlineData, flattenedOffChainData, fieldModifier);
}
}
}
for (let i = 0; i < onChainFields.length; i += 1) {
if (airline[onChainFields[i]]) {
airlineData[onChainFields[i]] = await airline[onChainFields[i]];
}
}
// Always append airline chain address as id property
airlineData.id = airline.address;
} catch (e) {
let message = 'Cannot get airline 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: airline.address,
},
};
}
return mapAirlineObjectToResponse(airlineData);
};
const fillAirlineList = async (path, fields, airlines, limit, startWith) => {
limit = limit ? parseInt(limit, 10) : DEFAULT_PAGE_SIZE;
const { items, nextStart } = paginate(airlines, limit, startWith, 'address');
let realItems = [], warningItems = [], realErrors = [];
const swaggerDocument = await DataFormatValidator.loadSchemaFromPath(SCHEMA_PATH, AIRLINE_SCHEMA_MODEL, fields.mapped, REVERSED_AIRLINE_FIELD_MAPPING);
const promises = [];
for (let airline of items) {
promises.push((() => {
let resolvedAirlineObject;
return resolveAirlineObject(airline, fields.toFlatten, fields.onChain)
.then((resolved) => {
resolvedAirlineObject = resolved;
DataFormatValidator.validate(
resolvedAirlineObject,
AIRLINE_SCHEMA_MODEL,
swaggerDocument.components.schemas,
config.dataFormatVersions.airlines,
undefined,
'airline',
fields.mapped,
);
realItems.push(_.omit(resolvedAirlineObject, fields.toDrop));
})
.catch((e) => {
if (e instanceof HttpValidationError) {
airline = {
error: 'Upstream airline data format validation failed: ' + e.toString(),
originalError: e.data.errors.map((err) => { return err.toString(); }).join(';'),
data: resolvedAirlineObject,
};
if (e.data && e.data.valid) {
warningItems.push(airline);
} else {
airline.data = e.data && e.data.data;
realErrors.push(airline);
}
} 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 fillAirlineList(path, fields, airlines, 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 airlines = await res.locals.wt.airlineDirectory.getOrganizations();
const { items, warnings, errors, next } = await fillAirlineList(req.path, calculateAirlinesFields(fields), airlines, 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 airline collection.'));
}
next(e);
}
};
const find = async (req, res, next) => {
try {
const fields = calculateAirlineFields(req.query.fields);
const swaggerDocument = await DataFormatValidator.loadSchemaFromPath(SCHEMA_PATH, AIRLINE_SCHEMA_MODEL, fields.mapped, REVERSED_AIRLINE_FIELD_MAPPING);
let resolvedAirline;
try {
resolvedAirline = await resolveAirlineObject(res.locals.wt.airline, fields.toFlatten, fields.onChain);
if (resolvedAirline.error) {
return next(new HttpBadGatewayError('airlineNotAccessible', resolvedAirline.error, 'Airline data is not accessible.'));
}
DataFormatValidator.validate(
resolvedAirline,
AIRLINE_SCHEMA_MODEL,
swaggerDocument.components.schemas,
config.dataFormatVersions.airlines,
undefined,
'airline',
fields.mapped
);
resolvedAirline = _.omit(resolvedAirline, fields.toDrop);
} catch (e) {
if (e instanceof HttpValidationError) {
const err = formatError(e);
err.data = resolvedAirline;
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(resolvedAirline);
} catch (e) {
return next(new HttpBadGatewayError('airlineNotAccessible', e.message, 'Airline data is not accessible.'));
}
};
const meta = async (req, res, next) => {
try {
const airlineApis = await res.locals.wt.airline.getWindingTreeApi();
const apiObject = await airlineApis.airline[0].toPlainObject([]);
return res.status(200).json({
address: res.locals.wt.airline.address,
orgJsonUri: apiObject.ref,
descriptionUri: apiObject.contents.descriptionUri,
flightsUri: apiObject.contents.flightsUri,
dataFormatVersion: apiObject.contents.dataFormatVersion,
});
} catch (e) {
return next(new HttpBadGatewayError('airlineNotAccessible', e.message, 'Airline data is not accessible.'));
}
};
module.exports = {
find,
findAll,
meta,
};