adding freight web and holding
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
export * from './esi/index';
|
||||
export * from './db';
|
||||
export * from './ref';
|
||||
export * from './esi/index';
|
||||
export * from './db';
|
||||
export * from './ref';
|
||||
export * from './third-party';
|
||||
@@ -1,14 +1,14 @@
|
||||
export * from './attribute';
|
||||
export * from './blueprint';
|
||||
export * from './category';
|
||||
export * from './effect';
|
||||
export * from './group';
|
||||
export * from './icon';
|
||||
export * from './market-group';
|
||||
export * from './meta-group';
|
||||
export * from './region';
|
||||
export * from './schematic';
|
||||
export * from './skill';
|
||||
export * from './solar-system';
|
||||
export * from './type';
|
||||
export * from './attribute';
|
||||
export * from './blueprint';
|
||||
export * from './category';
|
||||
export * from './effect';
|
||||
export * from './group';
|
||||
export * from './icon';
|
||||
export * from './market-group';
|
||||
export * from './meta-group';
|
||||
export * from './region';
|
||||
export * from './schematic';
|
||||
export * from './skill';
|
||||
export * from './solar-system';
|
||||
export * from './type';
|
||||
export * from './unit';
|
||||
@@ -1,53 +1,53 @@
|
||||
import fs from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import type { Unit } from './unit';
|
||||
import type { SolarSystem } from './solar-system';
|
||||
import type { Attribute } from './attribute';
|
||||
import type { Blueprint } from './blueprint';
|
||||
import type { Category } from './category';
|
||||
import type { Effect } from './effect';
|
||||
import type { Group } from './group';
|
||||
import type { Icon } from './icon';
|
||||
import type { MarketGroup } from './market-group';
|
||||
import type { MetaGroup } from './meta-group';
|
||||
import type { Region } from './region';
|
||||
import type { Schematic } from './schematic';
|
||||
import type { Skill } from './skill';
|
||||
import type { Type } from './type';
|
||||
|
||||
const dataSets = {
|
||||
loaded: false,
|
||||
dogma_attributes: {} as Record<string, Attribute>,
|
||||
blueprints: {} as Record<string, Blueprint>,
|
||||
categories: {} as Record<string, Category>,
|
||||
dogma_effects: {} as Record<string, Effect>,
|
||||
groups: {} as Record<string, Group>,
|
||||
icons: {} as Record<string, Icon>,
|
||||
market_groups: {} as Record<string, MarketGroup>,
|
||||
meta_groups: {} as Record<string, MetaGroup>,
|
||||
regions: {} as Record<string, Region>,
|
||||
schematics: {} as Record<string, Schematic>,
|
||||
skills: {} as Record<string, Skill>,
|
||||
solar_systems: {} as Record<string, SolarSystem>,
|
||||
types: {} as Record<string, Type>,
|
||||
units: {} as Record<string, Unit>,
|
||||
};
|
||||
export async function loadModels() {
|
||||
dataSets.dogma_attributes = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/dogma_attributes.json')).toString());
|
||||
dataSets.blueprints = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/blueprints.json')).toString());
|
||||
dataSets.categories = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/categories.json')).toString());
|
||||
dataSets.dogma_effects = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/dogma_effects.json')).toString());
|
||||
dataSets.groups = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/groups.json')).toString());
|
||||
dataSets.icons = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/icons.json')).toString());
|
||||
dataSets.market_groups = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/market_groups.json')).toString());
|
||||
dataSets.meta_groups = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/meta_groups.json')).toString());
|
||||
dataSets.regions = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/regions.json')).toString());
|
||||
dataSets.schematics = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/schematics.json')).toString());
|
||||
dataSets.skills = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/skills.json')).toString());
|
||||
dataSets.solar_systems = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/solar_systems.json')).toString());
|
||||
dataSets.types = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/types.json')).toString());
|
||||
dataSets.units = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/units.json')).toString());
|
||||
dataSets.loaded = true;
|
||||
}
|
||||
|
||||
export { dataSets };
|
||||
import fs from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import type { Unit } from './unit';
|
||||
import type { SolarSystem } from './solar-system';
|
||||
import type { Attribute } from './attribute';
|
||||
import type { Blueprint } from './blueprint';
|
||||
import type { Category } from './category';
|
||||
import type { Effect } from './effect';
|
||||
import type { Group } from './group';
|
||||
import type { Icon } from './icon';
|
||||
import type { MarketGroup } from './market-group';
|
||||
import type { MetaGroup } from './meta-group';
|
||||
import type { Region } from './region';
|
||||
import type { Schematic } from './schematic';
|
||||
import type { Skill } from './skill';
|
||||
import type { Type } from './type';
|
||||
|
||||
const dataSets = {
|
||||
loaded: false,
|
||||
dogma_attributes: {} as Record<string, Attribute>,
|
||||
blueprints: {} as Record<string, Blueprint>,
|
||||
categories: {} as Record<string, Category>,
|
||||
dogma_effects: {} as Record<string, Effect>,
|
||||
groups: {} as Record<string, Group>,
|
||||
icons: {} as Record<string, Icon>,
|
||||
market_groups: {} as Record<string, MarketGroup>,
|
||||
meta_groups: {} as Record<string, MetaGroup>,
|
||||
regions: {} as Record<string, Region>,
|
||||
schematics: {} as Record<string, Schematic>,
|
||||
skills: {} as Record<string, Skill>,
|
||||
solar_systems: {} as Record<string, SolarSystem>,
|
||||
types: {} as Record<string, Type>,
|
||||
units: {} as Record<string, Unit>,
|
||||
};
|
||||
export async function loadModels() {
|
||||
dataSets.dogma_attributes = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/dogma_attributes.json')).toString());
|
||||
dataSets.blueprints = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/blueprints.json')).toString());
|
||||
dataSets.categories = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/categories.json')).toString());
|
||||
dataSets.dogma_effects = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/dogma_effects.json')).toString());
|
||||
dataSets.groups = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/groups.json')).toString());
|
||||
dataSets.icons = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/icons.json')).toString());
|
||||
dataSets.market_groups = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/market_groups.json')).toString());
|
||||
dataSets.meta_groups = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/meta_groups.json')).toString());
|
||||
dataSets.regions = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/regions.json')).toString());
|
||||
dataSets.schematics = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/schematics.json')).toString());
|
||||
dataSets.skills = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/skills.json')).toString());
|
||||
dataSets.solar_systems = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/solar_systems.json')).toString());
|
||||
dataSets.types = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/types.json')).toString());
|
||||
dataSets.units = JSON.parse(fs.readFileSync(join(__dirname, '../../data/reference-data/units.json')).toString());
|
||||
dataSets.loaded = true;
|
||||
}
|
||||
|
||||
export { dataSets };
|
||||
|
||||
814
packages/eve/src/third-party/janice.ts
vendored
814
packages/eve/src/third-party/janice.ts
vendored
@@ -1,407 +1,407 @@
|
||||
/**
|
||||
* Janice API integration for EVE Online market appraisals and pricing.
|
||||
* This module provides interfaces and functions to interact with the Janice API.
|
||||
*/
|
||||
|
||||
const BASE_URL = 'https://janice.e-351.com/api/rest/v2';
|
||||
|
||||
/**
|
||||
* Represents an appraisal from the Janice API.
|
||||
*/
|
||||
export interface Appraisal {
|
||||
/** Unique identifier for the appraisal */
|
||||
id: number;
|
||||
/** Creation timestamp */
|
||||
created: string;
|
||||
/** Expiration timestamp */
|
||||
expires: string;
|
||||
/** Dataset timestamp */
|
||||
datasetTime: string;
|
||||
/** Appraisal code */
|
||||
code: string;
|
||||
/** Designation type */
|
||||
designation: AppraisalDesignation;
|
||||
/** Pricing strategy */
|
||||
pricing: AppraisalPricing;
|
||||
/** Pricing variant */
|
||||
pricingVariant: AppraisalPricingVariant;
|
||||
/** Price percentage */
|
||||
pricePercentage: number;
|
||||
/** Whether the appraisal is compactized */
|
||||
isCompactized: boolean;
|
||||
/** Failure messages */
|
||||
failures: string;
|
||||
/** Market information */
|
||||
market: PricerMarket;
|
||||
/** Total volume */
|
||||
totalVolume: number;
|
||||
/** Total packaged volume */
|
||||
totalPackagedVolume: number;
|
||||
/** Effective prices */
|
||||
effectivePrices: AppraisalValues;
|
||||
/** Immediate prices */
|
||||
immediatePrices: AppraisalValues;
|
||||
/** Top 5 average prices */
|
||||
top5AveragePrices: AppraisalValues;
|
||||
/** List of items in the appraisal */
|
||||
items: AppraisalItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Price values for an appraisal.
|
||||
*/
|
||||
export interface AppraisalValues {
|
||||
/** Total buy price */
|
||||
totalBuyPrice: number;
|
||||
/** Total split price */
|
||||
totalSplitPrice: number;
|
||||
/** Total sell price */
|
||||
totalSellPrice: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an item in an appraisal.
|
||||
*/
|
||||
export interface AppraisalItem {
|
||||
/** Item ID */
|
||||
id: number;
|
||||
/** Amount of the item */
|
||||
amount: number;
|
||||
/** Number of buy orders */
|
||||
buyOrderCount: number;
|
||||
/** Buy volume */
|
||||
buyVolume: number;
|
||||
/** Number of sell orders */
|
||||
sellOrderCount: number;
|
||||
/** Sell volume */
|
||||
sellVolume: number;
|
||||
/** Effective prices for the item */
|
||||
effectivePrices: AppraisalItemValues;
|
||||
/** Immediate prices for the item */
|
||||
immediatePrices: AppraisalItemValues;
|
||||
/** Top 5 average prices for the item */
|
||||
top5AveragePrices: AppraisalItemValues;
|
||||
/** Total volume */
|
||||
totalVolume: number;
|
||||
/** Total packaged volume */
|
||||
totalPackagedVolume: number;
|
||||
/** Item type information */
|
||||
itemType: ItemType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a pricer item from the API.
|
||||
*/
|
||||
export interface PricerItem {
|
||||
/** Date of the price data */
|
||||
date: string;
|
||||
/** Market information */
|
||||
market: PricerMarket;
|
||||
/** Number of buy orders */
|
||||
buyOrderCount: number;
|
||||
/** Buy volume */
|
||||
buyVolume: number;
|
||||
/** Number of sell orders */
|
||||
sellOrderCount: number;
|
||||
/** Sell volume */
|
||||
sellVolume: number;
|
||||
/** Immediate prices */
|
||||
immediatePrices: PricerItemValues;
|
||||
/** Top 5 average prices */
|
||||
top5AveragePrices: PricerItemValues;
|
||||
/** Item type information */
|
||||
itemType: ItemType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Price values for a pricer item.
|
||||
*/
|
||||
export interface PricerItemValues {
|
||||
/** Buy price */
|
||||
buyPrice: number;
|
||||
/** Split price */
|
||||
splitPrice: number;
|
||||
/** Sell price */
|
||||
sellPrice: number;
|
||||
/** 5-day median buy price */
|
||||
buyPrice5DayMedian: number;
|
||||
/** 5-day median split price */
|
||||
splitPrice5DayMedian: number;
|
||||
/** 5-day median sell price */
|
||||
sellPrice5DayMedian: number;
|
||||
/** 30-day median buy price */
|
||||
buyPrice30DayMedian: number;
|
||||
/** 30-day median split price */
|
||||
splitPrice30DayMedian: number;
|
||||
/** 30-day median sell price */
|
||||
sellPrice30DayMedian: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended price values for appraisal items.
|
||||
*/
|
||||
export interface AppraisalItemValues extends PricerItemValues {
|
||||
/** Total buy price */
|
||||
buyPriceTotal: number;
|
||||
/** Total split price */
|
||||
splitPriceTotal: number;
|
||||
/** Total sell price */
|
||||
sellPriceTotal: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an item type.
|
||||
*/
|
||||
export interface ItemType {
|
||||
/** EVE item ID */
|
||||
eid: number;
|
||||
/** Item name (optional) */
|
||||
name?: string;
|
||||
/** Item volume */
|
||||
volume: number;
|
||||
/** Packaged volume */
|
||||
packagedVolume: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumeration for appraisal designations.
|
||||
*/
|
||||
export enum AppraisalDesignation {
|
||||
Appraisal = 'appraisal',
|
||||
WantToBuy = 'wtb',
|
||||
WantToSell = 'wts',
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumeration for appraisal pricing strategies.
|
||||
*/
|
||||
export enum AppraisalPricing {
|
||||
Buy = 'buy',
|
||||
Split = 'split',
|
||||
Sell = 'sell',
|
||||
Purchase = 'purchase',
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumeration for appraisal pricing variants.
|
||||
*/
|
||||
export enum AppraisalPricingVariant {
|
||||
Immediate = 'immediate',
|
||||
Top5Percent = 'top5percent',
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a market in the pricer system.
|
||||
*/
|
||||
export interface PricerMarket {
|
||||
/** Market ID */
|
||||
id: number;
|
||||
/** Market name */
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Predefined list of available markets.
|
||||
*/
|
||||
export const markets: PricerMarket[] = [
|
||||
{ id: 2, name: 'Jita 4-4' },
|
||||
{ id: 3, name: 'R1O-GN' },
|
||||
{ id: 6, name: 'NPC' },
|
||||
{ id: 114, name: 'MJ-5F9' },
|
||||
{ id: 115, name: 'Amarr' },
|
||||
{ id: 116, name: 'Rens' },
|
||||
{ id: 117, name: 'Dodixie' },
|
||||
{ id: 118, name: 'Hek' },
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Simple cache for API responses to improve performance.
|
||||
*/
|
||||
const cache = new Map<string, { data: any; timestamp: number }>();
|
||||
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
/**
|
||||
* Clears the internal cache. Useful for testing.
|
||||
*/
|
||||
export function clearCache(): void {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a value is a positive number.
|
||||
*/
|
||||
export function isPositiveNumber(value: any): value is number {
|
||||
return typeof value === 'number' && value > 0 && isFinite(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a value is a non-empty string.
|
||||
*/
|
||||
export function isNonEmptyString(value: any): value is string {
|
||||
return typeof value === 'string' && value.trim().length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches price data for a single item type.
|
||||
* @param type_id - The EVE item type ID
|
||||
* @param market_id - The market ID (default: 2 for Jita)
|
||||
* @returns Promise resolving to PricerItem
|
||||
* @throws Error if API call fails or validation fails
|
||||
*/
|
||||
export const fetchPrice = async (type_id: number, market_id: number = 2): Promise<PricerItem> => {
|
||||
if (!isPositiveNumber(type_id)) {
|
||||
throw new Error('Invalid type_id: must be a positive number');
|
||||
}
|
||||
if (!isPositiveNumber(market_id)) {
|
||||
throw new Error('Invalid market_id: must be a positive number');
|
||||
}
|
||||
|
||||
const cacheKey = `price_${type_id}_${market_id}`;
|
||||
const cached = cache.get(cacheKey);
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/pricer/${type_id}?market=${market_id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-ApiKey': process.env.JANICE_KEY || '',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
cache.set(cacheKey, { data, timestamp: Date.now() });
|
||||
return data as PricerItem;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching price for type_id ${type_id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches price data for multiple item types.
|
||||
* @param type_ids - Array of EVE item type IDs
|
||||
* @param market_id - The market ID (default: 2 for Jita)
|
||||
* @returns Promise resolving to array of PricerItem
|
||||
* @throws Error if API call fails or validation fails
|
||||
*/
|
||||
export const fetchPrices = async (type_ids: number[], market_id: number = 2): Promise<PricerItem[]> => {
|
||||
if (!Array.isArray(type_ids) || type_ids.length === 0 || !type_ids.every(isPositiveNumber)) {
|
||||
throw new Error('Invalid type_ids: must be a non-empty array of positive numbers');
|
||||
}
|
||||
if (!isPositiveNumber(market_id)) {
|
||||
throw new Error('Invalid market_id: must be a positive number');
|
||||
}
|
||||
|
||||
const cacheKey = `prices_${type_ids.sort().join('_')}_${market_id}`;
|
||||
const cached = cache.get(cacheKey);
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/pricer?market=${market_id}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
'X-ApiKey': process.env.JANICE_KEY || '',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: type_ids.join('\n'),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
cache.set(cacheKey, { data, timestamp: Date.now() });
|
||||
return data as PricerItem[];
|
||||
} catch (error) {
|
||||
console.error(`Error fetching prices for type_ids ${type_ids}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches an appraisal by its code.
|
||||
* @param code - The appraisal code
|
||||
* @returns Promise resolving to Appraisal
|
||||
* @throws Error if API call fails or validation fails
|
||||
*/
|
||||
export const fetchAppraisal = async (code: string): Promise<Appraisal> => {
|
||||
if (!isNonEmptyString(code)) {
|
||||
throw new Error('Invalid code: must be a non-empty string');
|
||||
}
|
||||
|
||||
const cacheKey = `appraisal_${code}`;
|
||||
const cached = cache.get(cacheKey);
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/appraisal/${code}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-ApiKey': process.env.JANICE_KEY || '',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
cache.set(cacheKey, { data, timestamp: Date.now() });
|
||||
return data as Appraisal;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching appraisal for code ${code}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Appraises items from text input.
|
||||
* @param text - The text containing items to appraise
|
||||
* @param market_id - The market ID (default: 2 for Jita)
|
||||
* @returns Promise resolving to Appraisal
|
||||
* @throws Error if API call fails or validation fails
|
||||
*/
|
||||
export const appraiseItems = async (text: string, market_id: number = 2): Promise<Appraisal> => {
|
||||
if (!isNonEmptyString(text)) {
|
||||
throw new Error('Invalid text: must be a non-empty string');
|
||||
}
|
||||
if (!isPositiveNumber(market_id)) {
|
||||
throw new Error('Invalid market_id: must be a positive number');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/appraisal?market=${market_id}&persist=true&compactize=true&pricePercentage=1`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
'X-ApiKey': process.env.JANICE_KEY || '',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: text,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data as Appraisal;
|
||||
} catch (error) {
|
||||
console.error('Error appraising items:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Janice API integration for EVE Online market appraisals and pricing.
|
||||
* This module provides interfaces and functions to interact with the Janice API.
|
||||
*/
|
||||
|
||||
const BASE_URL = 'https://janice.e-351.com/api/rest/v2';
|
||||
|
||||
/**
|
||||
* Represents an appraisal from the Janice API.
|
||||
*/
|
||||
export interface Appraisal {
|
||||
/** Unique identifier for the appraisal */
|
||||
id: number;
|
||||
/** Creation timestamp */
|
||||
created: string;
|
||||
/** Expiration timestamp */
|
||||
expires: string;
|
||||
/** Dataset timestamp */
|
||||
datasetTime: string;
|
||||
/** Appraisal code */
|
||||
code: string;
|
||||
/** Designation type */
|
||||
designation: AppraisalDesignation;
|
||||
/** Pricing strategy */
|
||||
pricing: AppraisalPricing;
|
||||
/** Pricing variant */
|
||||
pricingVariant: AppraisalPricingVariant;
|
||||
/** Price percentage */
|
||||
pricePercentage: number;
|
||||
/** Whether the appraisal is compactized */
|
||||
isCompactized: boolean;
|
||||
/** Failure messages */
|
||||
failures: string;
|
||||
/** Market information */
|
||||
market: PricerMarket;
|
||||
/** Total volume */
|
||||
totalVolume: number;
|
||||
/** Total packaged volume */
|
||||
totalPackagedVolume: number;
|
||||
/** Effective prices */
|
||||
effectivePrices: AppraisalValues;
|
||||
/** Immediate prices */
|
||||
immediatePrices: AppraisalValues;
|
||||
/** Top 5 average prices */
|
||||
top5AveragePrices: AppraisalValues;
|
||||
/** List of items in the appraisal */
|
||||
items: AppraisalItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Price values for an appraisal.
|
||||
*/
|
||||
export interface AppraisalValues {
|
||||
/** Total buy price */
|
||||
totalBuyPrice: number;
|
||||
/** Total split price */
|
||||
totalSplitPrice: number;
|
||||
/** Total sell price */
|
||||
totalSellPrice: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an item in an appraisal.
|
||||
*/
|
||||
export interface AppraisalItem {
|
||||
/** Item ID */
|
||||
id: number;
|
||||
/** Amount of the item */
|
||||
amount: number;
|
||||
/** Number of buy orders */
|
||||
buyOrderCount: number;
|
||||
/** Buy volume */
|
||||
buyVolume: number;
|
||||
/** Number of sell orders */
|
||||
sellOrderCount: number;
|
||||
/** Sell volume */
|
||||
sellVolume: number;
|
||||
/** Effective prices for the item */
|
||||
effectivePrices: AppraisalItemValues;
|
||||
/** Immediate prices for the item */
|
||||
immediatePrices: AppraisalItemValues;
|
||||
/** Top 5 average prices for the item */
|
||||
top5AveragePrices: AppraisalItemValues;
|
||||
/** Total volume */
|
||||
totalVolume: number;
|
||||
/** Total packaged volume */
|
||||
totalPackagedVolume: number;
|
||||
/** Item type information */
|
||||
itemType: ItemType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a pricer item from the API.
|
||||
*/
|
||||
export interface PricerItem {
|
||||
/** Date of the price data */
|
||||
date: string;
|
||||
/** Market information */
|
||||
market: PricerMarket;
|
||||
/** Number of buy orders */
|
||||
buyOrderCount: number;
|
||||
/** Buy volume */
|
||||
buyVolume: number;
|
||||
/** Number of sell orders */
|
||||
sellOrderCount: number;
|
||||
/** Sell volume */
|
||||
sellVolume: number;
|
||||
/** Immediate prices */
|
||||
immediatePrices: PricerItemValues;
|
||||
/** Top 5 average prices */
|
||||
top5AveragePrices: PricerItemValues;
|
||||
/** Item type information */
|
||||
itemType: ItemType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Price values for a pricer item.
|
||||
*/
|
||||
export interface PricerItemValues {
|
||||
/** Buy price */
|
||||
buyPrice: number;
|
||||
/** Split price */
|
||||
splitPrice: number;
|
||||
/** Sell price */
|
||||
sellPrice: number;
|
||||
/** 5-day median buy price */
|
||||
buyPrice5DayMedian: number;
|
||||
/** 5-day median split price */
|
||||
splitPrice5DayMedian: number;
|
||||
/** 5-day median sell price */
|
||||
sellPrice5DayMedian: number;
|
||||
/** 30-day median buy price */
|
||||
buyPrice30DayMedian: number;
|
||||
/** 30-day median split price */
|
||||
splitPrice30DayMedian: number;
|
||||
/** 30-day median sell price */
|
||||
sellPrice30DayMedian: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended price values for appraisal items.
|
||||
*/
|
||||
export interface AppraisalItemValues extends PricerItemValues {
|
||||
/** Total buy price */
|
||||
buyPriceTotal: number;
|
||||
/** Total split price */
|
||||
splitPriceTotal: number;
|
||||
/** Total sell price */
|
||||
sellPriceTotal: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an item type.
|
||||
*/
|
||||
export interface ItemType {
|
||||
/** EVE item ID */
|
||||
eid: number;
|
||||
/** Item name (optional) */
|
||||
name?: string;
|
||||
/** Item volume */
|
||||
volume: number;
|
||||
/** Packaged volume */
|
||||
packagedVolume: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumeration for appraisal designations.
|
||||
*/
|
||||
export enum AppraisalDesignation {
|
||||
Appraisal = 'appraisal',
|
||||
WantToBuy = 'wtb',
|
||||
WantToSell = 'wts',
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumeration for appraisal pricing strategies.
|
||||
*/
|
||||
export enum AppraisalPricing {
|
||||
Buy = 'buy',
|
||||
Split = 'split',
|
||||
Sell = 'sell',
|
||||
Purchase = 'purchase',
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumeration for appraisal pricing variants.
|
||||
*/
|
||||
export enum AppraisalPricingVariant {
|
||||
Immediate = 'immediate',
|
||||
Top5Percent = 'top5percent',
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a market in the pricer system.
|
||||
*/
|
||||
export interface PricerMarket {
|
||||
/** Market ID */
|
||||
id: number;
|
||||
/** Market name */
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Predefined list of available markets.
|
||||
*/
|
||||
export const markets: PricerMarket[] = [
|
||||
{ id: 2, name: 'Jita 4-4' },
|
||||
{ id: 3, name: 'R1O-GN' },
|
||||
{ id: 6, name: 'NPC' },
|
||||
{ id: 114, name: 'MJ-5F9' },
|
||||
{ id: 115, name: 'Amarr' },
|
||||
{ id: 116, name: 'Rens' },
|
||||
{ id: 117, name: 'Dodixie' },
|
||||
{ id: 118, name: 'Hek' },
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Simple cache for API responses to improve performance.
|
||||
*/
|
||||
const cache = new Map<string, { data: any; timestamp: number }>();
|
||||
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
/**
|
||||
* Clears the internal cache. Useful for testing.
|
||||
*/
|
||||
export function clearCache(): void {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a value is a positive number.
|
||||
*/
|
||||
export function isPositiveNumber(value: any): value is number {
|
||||
return typeof value === 'number' && value > 0 && isFinite(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a value is a non-empty string.
|
||||
*/
|
||||
export function isNonEmptyString(value: any): value is string {
|
||||
return typeof value === 'string' && value.trim().length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches price data for a single item type.
|
||||
* @param type_id - The EVE item type ID
|
||||
* @param market_id - The market ID (default: 2 for Jita)
|
||||
* @returns Promise resolving to PricerItem
|
||||
* @throws Error if API call fails or validation fails
|
||||
*/
|
||||
export const fetchPrice = async (type_id: number, market_id: number = 2): Promise<PricerItem> => {
|
||||
if (!isPositiveNumber(type_id)) {
|
||||
throw new Error('Invalid type_id: must be a positive number');
|
||||
}
|
||||
if (!isPositiveNumber(market_id)) {
|
||||
throw new Error('Invalid market_id: must be a positive number');
|
||||
}
|
||||
|
||||
const cacheKey = `price_${type_id}_${market_id}`;
|
||||
const cached = cache.get(cacheKey);
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/pricer/${type_id}?market=${market_id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-ApiKey': process.env.JANICE_KEY || '',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
cache.set(cacheKey, { data, timestamp: Date.now() });
|
||||
return data as PricerItem;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching price for type_id ${type_id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches price data for multiple item types.
|
||||
* @param type_ids - Array of EVE item type IDs
|
||||
* @param market_id - The market ID (default: 2 for Jita)
|
||||
* @returns Promise resolving to array of PricerItem
|
||||
* @throws Error if API call fails or validation fails
|
||||
*/
|
||||
export const fetchPrices = async (type_ids: number[], market_id: number = 2): Promise<PricerItem[]> => {
|
||||
if (!Array.isArray(type_ids) || type_ids.length === 0 || !type_ids.every(isPositiveNumber)) {
|
||||
throw new Error('Invalid type_ids: must be a non-empty array of positive numbers');
|
||||
}
|
||||
if (!isPositiveNumber(market_id)) {
|
||||
throw new Error('Invalid market_id: must be a positive number');
|
||||
}
|
||||
|
||||
const cacheKey = `prices_${type_ids.sort().join('_')}_${market_id}`;
|
||||
const cached = cache.get(cacheKey);
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/pricer?market=${market_id}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
'X-ApiKey': process.env.JANICE_KEY || '',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: type_ids.join('\n'),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
cache.set(cacheKey, { data, timestamp: Date.now() });
|
||||
return data as PricerItem[];
|
||||
} catch (error) {
|
||||
console.error(`Error fetching prices for type_ids ${type_ids}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches an appraisal by its code.
|
||||
* @param code - The appraisal code
|
||||
* @returns Promise resolving to Appraisal
|
||||
* @throws Error if API call fails or validation fails
|
||||
*/
|
||||
export const fetchAppraisal = async (code: string): Promise<Appraisal> => {
|
||||
if (!isNonEmptyString(code)) {
|
||||
throw new Error('Invalid code: must be a non-empty string');
|
||||
}
|
||||
|
||||
const cacheKey = `appraisal_${code}`;
|
||||
const cached = cache.get(cacheKey);
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/appraisal/${code}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-ApiKey': process.env.JANICE_KEY || '',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
cache.set(cacheKey, { data, timestamp: Date.now() });
|
||||
return data as Appraisal;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching appraisal for code ${code}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Appraises items from text input.
|
||||
* @param text - The text containing items to appraise
|
||||
* @param market_id - The market ID (default: 2 for Jita)
|
||||
* @returns Promise resolving to Appraisal
|
||||
* @throws Error if API call fails or validation fails
|
||||
*/
|
||||
export const appraiseItems = async (text: string, market_id: number = 2, apiKey: string | undefined = process.env.JANICE_KEY): Promise<Appraisal> => {
|
||||
if (!isNonEmptyString(text)) {
|
||||
throw new Error('Invalid text: must be a non-empty string');
|
||||
}
|
||||
if (!isPositiveNumber(market_id)) {
|
||||
throw new Error('Invalid market_id: must be a positive number');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/appraisal?market=${market_id}&persist=true&compactize=true&pricePercentage=1`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
'X-ApiKey': apiKey || '',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: text,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data as Appraisal;
|
||||
} catch (error) {
|
||||
console.error('Error appraising items:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,73 +1,73 @@
|
||||
import fs from 'node:fs';
|
||||
import { chain } from 'stream-chain';
|
||||
import { parser } from 'stream-json';
|
||||
import { streamObject } from 'stream-json/streamers/StreamObject';
|
||||
import { create, insert, search } from '@orama/orama';
|
||||
import { normalize } from '@star-kitten/util/text.js';
|
||||
import { getType, type Type } from '@/models/type';
|
||||
|
||||
const db = create({
|
||||
schema: {
|
||||
type_id: 'number',
|
||||
name: {
|
||||
en: 'string',
|
||||
de: 'string',
|
||||
fr: 'string',
|
||||
ru: 'string',
|
||||
ja: 'string',
|
||||
zh: 'string',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export async function initializeTypeSearch() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const pipeline = chain([fs.createReadStream('../../data/reference-data/types.json'), parser(), streamObject(), (data) => data]);
|
||||
|
||||
pipeline.on('data', async ({ value }) => {
|
||||
if (value && value.market_group_id && value.published) {
|
||||
try {
|
||||
await addType(value);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
pipeline.on('error', reject);
|
||||
pipeline.on('end', resolve);
|
||||
});
|
||||
}
|
||||
|
||||
const addType = async (type: Type) =>
|
||||
await insert(db, {
|
||||
type_id: type.type_id,
|
||||
name: type.name,
|
||||
});
|
||||
|
||||
export async function typeSearch(name: string) {
|
||||
let now = Date.now();
|
||||
const normalizedName = normalize(name);
|
||||
if (normalizedName.length > 100) return null;
|
||||
const results = await search(db, {
|
||||
term: normalizedName,
|
||||
limit: 1,
|
||||
tolerance: 0,
|
||||
});
|
||||
if (!results || results.count === 0) return null;
|
||||
now = Date.now();
|
||||
const type = await getType(results.hits[0].document.type_id);
|
||||
return type;
|
||||
}
|
||||
|
||||
export async function typeSearchAutoComplete(name: string) {
|
||||
const normalizedName = normalize(name);
|
||||
if (normalizedName.length > 100) return null;
|
||||
const results = await search(db, {
|
||||
term: normalizedName,
|
||||
});
|
||||
if (!results || results.count === 0) return null;
|
||||
return results.hits.map((hit) => ({
|
||||
name: hit.document.name.en,
|
||||
value: hit.document.name.en,
|
||||
}));
|
||||
}
|
||||
import fs from 'node:fs';
|
||||
import { chain } from 'stream-chain';
|
||||
import { parser } from 'stream-json';
|
||||
import { streamObject } from 'stream-json/streamers/StreamObject';
|
||||
import { create, insert, search } from '@orama/orama';
|
||||
import { normalize } from '@star-kitten/util/text.js';
|
||||
import { getType, type Type } from '@/models/type';
|
||||
|
||||
const db = create({
|
||||
schema: {
|
||||
type_id: 'number',
|
||||
name: {
|
||||
en: 'string',
|
||||
de: 'string',
|
||||
fr: 'string',
|
||||
ru: 'string',
|
||||
ja: 'string',
|
||||
zh: 'string',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export async function initializeTypeSearch() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const pipeline = chain([fs.createReadStream('../../data/reference-data/types.json'), parser(), streamObject(), (data) => data]);
|
||||
|
||||
pipeline.on('data', async ({ value }) => {
|
||||
if (value && value.market_group_id && value.published) {
|
||||
try {
|
||||
await addType(value);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
pipeline.on('error', reject);
|
||||
pipeline.on('end', resolve);
|
||||
});
|
||||
}
|
||||
|
||||
const addType = async (type: Type) =>
|
||||
await insert(db, {
|
||||
type_id: type.type_id,
|
||||
name: type.name,
|
||||
});
|
||||
|
||||
export async function typeSearch(name: string) {
|
||||
let now = Date.now();
|
||||
const normalizedName = normalize(name);
|
||||
if (normalizedName.length > 100) return null;
|
||||
const results = await search(db, {
|
||||
term: normalizedName,
|
||||
limit: 1,
|
||||
tolerance: 0,
|
||||
});
|
||||
if (!results || results.count === 0) return null;
|
||||
now = Date.now();
|
||||
const type = await getType(results.hits[0].document.type_id);
|
||||
return type;
|
||||
}
|
||||
|
||||
export async function typeSearchAutoComplete(name: string) {
|
||||
const normalizedName = normalize(name);
|
||||
if (normalizedName.length > 100) return null;
|
||||
const results = await search(db, {
|
||||
term: normalizedName,
|
||||
});
|
||||
if (!results || results.count === 0) return null;
|
||||
return results.hits.map((hit) => ({
|
||||
name: hit.document.name.en,
|
||||
value: hit.document.name.en,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,25 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "@star-kitten/discord",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": false,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"composite": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@data/*": ["./data/*"]
|
||||
|
||||
Reference in New Issue
Block a user