Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lite api fix #336

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions graphql-yoga.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import useTwitch from './plugins/plugin-twitch.mjs';
import useNightbot from './plugins/plugin-nightbot.mjs';
import usePlayground from './plugins/plugin-playground.mjs';
import useOptionMethod from './plugins/plugin-option-method.mjs';
import useLiteApi from './plugins/plugin-lite-api.mjs';

let dataAPI, yoga;

Expand Down Expand Up @@ -46,6 +47,7 @@ export default async function getYoga(env) {
useTwitch(env),
usePlayground(),
useNightbot(env),
useLiteApi(env),
useHttpServer(env),
useCacheMachine(env),
],
Expand Down
81 changes: 37 additions & 44 deletions index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ import graphQLOptions from './utils/graphql-options.mjs';
import cacheMachine from './utils/cache-machine.mjs';
import fetchWithTimeout from './utils/fetch-with-timeout.mjs';

import { getNightbotResponse } from './plugins/plugin-nightbot.mjs';
import { getNightbotResponse, nightbotPaths } from './plugins/plugin-nightbot.mjs';
import { getTwitchResponse } from './plugins/plugin-twitch.mjs';
import { getLiteApiResponse, liteApiPathRegex } from './plugins/plugin-lite-api.mjs';

let dataAPI;

Expand Down Expand Up @@ -108,32 +109,6 @@ async function graphqlHandler(request, env, ctx) {
//console.log(`Skipping cache in ${ENVIRONMENT} environment`);
}

// if an HTTP GraphQL server is configured, pass the request to that
if (env.USE_ORIGIN === 'true') {
try {
const serverUrl = `https://api.tarkov.dev${graphQLOptions.baseEndpoint}`;
const queryResult = await fetchWithTimeout(serverUrl, {
method: request.method,
body: JSON.stringify({
query,
variables,
}),
headers: {
'Content-Type': 'application/json',
'cache-check-complete': 'true',
},
timeout: 20000
});
if (queryResult.status !== 200) {
throw new Error(`${queryResult.status} ${await queryResult.text()}`);
}
console.log('Request served from graphql server');
return new Response(await queryResult.text(), responseOptions);
} catch (error) {
console.error(`Error getting response from GraphQL server: ${error}`);
}
}

const context = graphqlUtil.getDefaultContext(dataAPI, requestId);
let result, ttl;
try {
Expand Down Expand Up @@ -206,47 +181,65 @@ export default {
const requestStart = new Date();
const url = new URL(request.url);

let response;

try {
if (url.pathname === '/twitch') {
response = await getTwitchResponse(env);
const response = await getTwitchResponse(env);
if (graphQLOptions.cors) {
setCors(response, graphQLOptions.cors);
}
return response;
}

if (url.pathname === graphQLOptions.playgroundEndpoint) {
//response = playground(request, graphQLOptions);
response = graphiql(graphQLOptions);
return graphiql(graphQLOptions);
}

if (graphQLOptions.forwardUnmatchedRequestsToOrigin) {
return fetch(request);
if (!nightbotPaths.includes(url.pathname) && !url.pathname.match(liteApiPathRegex) && url.pathname !== graphQLOptions.baseEndpoint) {
return new Response('Not found', { status: 404 });
}

// if an origin server is configured, pass the request
if (env.USE_ORIGIN === 'true') {
try {
const response = await fetchWithTimeout(request.clone(), {
headers: {
'cache-check-complete': 'true',
},
timeout: 20000
});
if (response.status !== 200) {
throw new Error(`${response.status} ${await response.text()}`);
}
console.log('Request served from origin server');
return response;
} catch (error) {
console.error(`Error getting response from origin server: ${error}`);
}
}

if (url.pathname === '/webhook/nightbot' ||
url.pathname === '/webhook/stream-elements' ||
url.pathname === '/webhook/moobot'
) {
response = await getNightbotResponse(request, url, env, ctx);
if (nightbotPaths.includes(url.pathname)) {
return await getNightbotResponse(request, url, env, ctx);
}

if (url.pathname.match(liteApiPathRegex)) {
return await getLiteApiResponse(request, url, env, ctx);
}

if (url.pathname === graphQLOptions.baseEndpoint) {
response = await graphqlHandler(request, env, ctx);
const response = await graphqlHandler(request, env, ctx);
if (graphQLOptions.cors) {
setCors(response, graphQLOptions.cors);
}
return response;
}

if (!response) {
response = new Response('Not found', { status: 404 });
}
console.log(`Response time: ${new Date() - requestStart} ms`);
return response;
return new Response('Not found', { status: 404 });
} catch (err) {
console.log(err);
return new Response(graphQLOptions.debug ? err : 'Something went wrong', { status: 500 });
} finally {
console.log(`Response time: ${new Date() - requestStart} ms`);
}
},
};
178 changes: 178 additions & 0 deletions plugins/plugin-lite-api.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import cacheMachine from '../utils/cache-machine.mjs';
import DataSource from '../datasources/index.mjs';
import graphqlUtil from '../utils/graphql-util.mjs';

export const liteApiPathRegex = /\/api\/v1(?<gameMode>\/\w+)?\/(?<endpoint>item[\w\/]*)/;

const currencyMap = {
RUB: '₽',
USD: '$',
EUR: '€',
};

export async function getLiteApiResponse(request, url, env, serverContext) {
let q, lang, uid, tags, sort, sort_direction;
if (request.method.toUpperCase() === 'GET') {
q = url.searchParams.get('q');
lang = url.searchParams.get('lang') ?? 'en';
uid = url.searchParams.get('uid');
tags = url.searchParams.get('tags')?.split(',');
sort = url.searchParams.get('sort');
sort_direction = url.searchParams.get('sort_direction');
} else if (request.method.toUpperCase() === 'POST') {
const body = await request.json();
q = body.q;
lang = body.lang ?? 'en';
uid = body.uid;
tags = body.tags?.split(',');
sort = body.sort;
sort_direction = body.sort_direction;
} else {
return new Response(null, {
status: 405,
headers: { 'cache-control': 'public, max-age=2592000' },
});
}

const pathInfo = url.pathname.match(liteApiPathRegex);

const gameMode = pathInfo.groups.gameMode || 'regular';

const endpoint = pathInfo.groups.endpoint;

let key;
if (env.SKIP_CACHE !== 'true' && !request.headers.has('cache-check-complete')) {
const requestStart = new Date();
key = await cacheMachine.createKey(env, url.pathname, { q, lang, gameMode, uid, tags, sort, sort_direction });
const cachedResponse = await cacheMachine.get(env, {key});
if (cachedResponse) {
// Construct a new response with the cached data
const newResponse = new Response(cachedResponse);
// Add a custom 'X-CACHE: HIT' header so we know the request hit the cache
newResponse.headers.append('X-CACHE', 'HIT');
console.log(`Request served from cache: ${new Date() - requestStart} ms`);
// Return the new cached response
request.cached = true;
return newResponse;
} else {
console.log('no cached response');
}
} else {
//console.log(`Skipping cache in ${ENVIRONMENT} environment`);
}
const data = new DataSource(env);
const context = graphqlUtil.getDefaultContext(data);

const info = graphqlUtil.getGenericInfo(lang, gameMode);

function toLiteApiItem(item) {
const bestTraderSell = item.traderPrices.reduce((best, current) => {
if (!best || current.priceRUB > best.priceRUB) {
return current;
}
return best;
}, undefined);
return {
uid: item.id,
name: data.worker.item.getLocale(item.name, context, info),
tags: item.types,
shortName: data.worker.item.getLocale(item.shortName, context, info),
price: item.lastLowPrice,
basePrice: item.basePrice,
avg24hPrice: item.avg24hPrice,
//avg7daysPrice: null,
traderName: bestTraderSell ? bestTraderSell.name : null,
traderPrice: bestTraderSell ? bestTraderSell.price : null,
traderPriceCur: bestTraderSell ? currencyMap[bestTraderSell.currency] : null,
updated: item.updated,
slots: item.width * item.height,
diff24h: item.changeLast48h,
//diff7days: null,
icon: item.iconLink,
link: item.link,
wikiLink: item.wikiLink,
img: item.gridImageLink,
imgBig: item.inspectImageLink,
img512: item.image512pxLink,
image8x: item.image8xLink,
bsgId: item.id,
isFunctional: true, // !item.types.includes('gun'),
reference: 'https://tarkov.dev',
};
}

let items, ttl;
const responseOptions = {
headers: {
'Content-Type': 'application/json',
},
};
try {
if (endpoint.startsWith('items')) {
items = await data.worker.item.getAllItems(context, info);
if (endpoint.endsWith('/download')) {
responseOptions.headers['Content-Disposition'] = 'attachment; filename="items.json"';
}
if (tags) {
items = await data.worker.item.getItemsByTypes(context, info, tags, items);
}
}
if (!items && endpoint.startsWith('item')) {
if (!q && !uid) {
throw new Error('The item request requires either a q or uid parameter');
}
if (q) {
items = await data.worker.item.getItemsByName(context, info, q);
} else if (uid) {
items = [await data.worker.item.getItem(context, info, uid)];
}
}
items = items.map(toLiteApiItem);
ttl = data.getRequestTtl(context.requestId);
} catch (error) {
return new Response(error.message, {status: 400});
} finally {
data.clearRequestData(context.requestId);
}
if (sort && items?.length) {
items.sort((a, b) => {
let aValue = sort_direction === 'desc' ? b[sort] : a[sort];
let bValue = sort_direction === 'desc' ? a[sort] : b[sort];
if (sort === 'updated') {
aValue = new Date(aValue);
bValue = new Date(bValue);
}
if (typeof aValue === 'string') {
return aValue.localeCompare(bValue, lang);
}
return aValue - bValue;
});
}
const responseBody = JSON.stringify(items ?? [], null, 4);

// Update the cache with the results of the query
if (env.SKIP_CACHE !== 'true' && ttl > 0) {
const putCachePromise = cacheMachine.put(env, responseBody, { key, query: url.pathname, variables: { q, lang, gameMode, uid, tags, sort, sort_direction }, ttl: String(ttl)});
// using waitUntil doens't hold up returning a response but keeps the worker alive as long as needed
if (request.ctx?.waitUntil) {
request.ctx.waitUntil(putCachePromise);
} else if (serverContext.waitUntil) {
serverContext.waitUntil(putCachePromise);
}
}

return new Response(responseBody, responseOptions);
}

export default function useLiteApi(env) {
return {
async onRequest({ request, url, endResponse, serverContext, fetchAPI }) {
if (!url.pathname.match(liteApiPathRegex)) {
return;
}
const response = await getLiteApiResponse(request, url, env, serverContext);

endResponse(response);
},
}
}
4 changes: 2 additions & 2 deletions plugins/plugin-nightbot.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ function capitalize(s) {
return s && s[0].toUpperCase() + s.slice(1);
}

const usePaths = [
export const nightbotPaths = [
'/webhook/nightbot',
'/webhook/stream-elements',
'/webhook/moobot',
Expand Down Expand Up @@ -88,7 +88,7 @@ export async function getNightbotResponse(request, url, env, serverContext) {
export default function useNightbot(env) {
return {
async onRequest({ request, url, endResponse, serverContext, fetchAPI }) {
if (!usePaths.includes(url.pathname)) {
if (!nightbotPaths.includes(url.pathname)) {
return;
}
const response = await getNightbotResponse(request, url, env, serverContext);
Expand Down
Loading