usda-food-data-api-server
Version:
USDA Food Data API server for self-hosted API access to the USDA Food Data database
124 lines (110 loc) • 3.79 kB
text/typescript
import { dump } from "js-yaml";
import { BaseContext } from "koa";
import Router from "koa-router";
import {
defaultFoodQuery,
DefinedFoodQuery,
getFood
} from "../helpers/usda-v1.js";
import { splitNumber } from "./koa-normalize-arguments.js";
const router = new Router({
prefix: "/v1"
});
export default router;
/**
* Given a Koa.JS query param, returns a valid format type for formatDocument
* @param input koa query param
* @returns yaml or json
*/
function sanitizeFormatType(input: string | string[] | undefined) {
let str = (input instanceof Array ? input[0] : input) || "json";
return str.toLowerCase() === "yaml" ? "yaml" : "json";
}
/**
* Given a raw query or POST body, normalizes the arguments.
* @param dirty The dirty request parameters
* @returns Normalized request parameters
*/
function getArgs(dirty: any) {
const results = { ...defaultFoodQuery, ...dirty };
results.pageSize = Math.max(1, Number(results.pageSize));
results.pageNumber = Math.max(0, Number(results.pageNumber));
results.nutrients = splitNumber(results.nutrients);
results.format = sanitizeFormatType(results.format);
return <DefinedFoodQuery>results;
}
/**
* Handles a generic API request, taking a response and formatting it into the
* requested format.
*
* @param ctx Koa ctx
* @param docs The documents to be served
* @param format json or yaml format
*/
function handleApi(ctx: BaseContext, docs: any, format: string) {
if (docs) {
let contentType = format === "yaml" ? "text/yaml" : "application/json";
ctx.set("Content-type", contentType);
if (format === "yaml") {
ctx.body = dump(docs);
} else {
ctx.body = JSON.stringify(docs);
}
} else {
ctx.status = 404;
ctx.body = `Not found`; // TODO Match USDA Api
}
}
/**
* For the given fdcId, returns a FoodItem
*/
router.get("/food/:fdcId", async function (ctx) {
const fdcId = splitNumber(ctx.params.fdcId);
const args = getArgs(ctx.request.query);
handleApi(ctx, (await getFood({ fdcId }, args))[0], args.format);
});
/**
* For the given context, finds the requested FoodItem for the given fdcIds.
* Handles both the POST and GET requests.
* @param ctx Koa ctx
*/
async function handleFdcIdsApi(ctx: any) {
const post = ctx.request.method === "POST";
const params = ctx.request[post ? "body" : "query"];
const fdcIds = splitNumber(params.fdcIds || "");
const args = getArgs(params);
handleApi(
ctx,
await getFood({ fdcId: { $in: fdcIds } }, args),
args.format
);
}
router.get("/foods", handleFdcIdsApi);
router.post("/foods", handleFdcIdsApi);
/**
* For the given context, finds the required FoodItem's and returns them.
* Covers both /foods/list and /foods/search since they offer the same
* functionality, with the difference of a full text search.
* @param ctx Koa context
*/
async function handleSearchApi(ctx: any) {
const params =
ctx.request.method === "GET" ? ctx.request.query : ctx.request.body;
const { query } = params;
const args = getArgs(params);
const opts = query ? { $text: { $search: query } } : {};
handleApi(ctx, await getFood(opts, args), args.format);
}
router.get("/foods/list", handleSearchApi);
router.post("/foods/list", handleSearchApi);
router.get("/foods/search", handleSearchApi);
router.post("/foods/search", handleSearchApi);
// TODO - specs
router.get("/json-spec", async (ctx) => {
ctx.status = 404;
ctx.body = "Not found";
});
router.get("/yaml-spec", async (ctx) => {
ctx.status = 404;
ctx.body = "Not found";
});