diff --git a/bin/yahoo-finance.js b/bin/yahoo-finance.js index 4e028fa0..4bd137cc 100755 --- a/bin/yahoo-finance.js +++ b/bin/yahoo-finance.js @@ -30,7 +30,14 @@ function decodeArgs(stringArgs) { (async function() { const args = decodeArgs(argsAsStrings); - const result = await yahooFinance[moduleName](...args); + let result; + try { + result = await yahooFinance[moduleName](...args); + } catch (error) { + // No need for full stack trace for CLI scripts + console.error("Exiting with " + error.name + ": " + error.message); + process.exit(1); + } if (process.stdout.isTTY) console.dir(result, { depth: null, colors: true }); diff --git a/schema.json b/schema.json index fa9d2ee8..483619fc 100644 --- a/schema.json +++ b/schema.json @@ -1,6 +1,18 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { + "AutocOptions": { + "additionalProperties": false, + "properties": { + "lang": { + "type": "string" + }, + "region": { + "type": "number" + } + }, + "type": "object" + }, "AutocResult": { "additionalProperties": false, "properties": { @@ -52,6 +64,57 @@ ], "type": "object" }, + "HistoricalOptions": { + "additionalProperties": false, + "properties": { + "events": { + "type": "string" + }, + "includeAdjustedClose": { + "type": "boolean" + }, + "interval": { + "enum": [ + "1d", + "1wk", + "1mo" + ], + "type": "string" + }, + "period1": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "string" + }, + { + "type": "number" + } + ] + }, + "period2": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "string" + }, + { + "type": "number" + } + ] + } + }, + "required": [ + "period1" + ], + "type": "object" + }, "HistoricalResult": { "items": { "$ref": "#/definitions/HistoricalRow" @@ -537,6 +600,45 @@ ], "type": "object" }, + "SearchOptions": { + "additionalProperties": false, + "properties": { + "enableCb": { + "type": "boolean" + }, + "enableEnhancedTrivialQuery": { + "type": "boolean" + }, + "enableFuzzyQuery": { + "type": "boolean" + }, + "enableNavLinks": { + "type": "boolean" + }, + "lang": { + "type": "string" + }, + "multiQuoteQueryId": { + "type": "string" + }, + "newsCount": { + "type": "number" + }, + "newsQueryId": { + "type": "string" + }, + "quotesCount": { + "type": "number" + }, + "quotesQueryId": { + "type": "string" + }, + "region": { + "type": "string" + } + }, + "type": "object" + }, "SearchQuoteNonYahoo": { "additionalProperties": false, "properties": { diff --git a/src/lib/errors.js b/src/lib/errors.js index 6c131478..6a76706f 100644 --- a/src/lib/errors.js +++ b/src/lib/errors.js @@ -1,4 +1,11 @@ -class BadRequestError extends Error {} -class HTTPError extends Error {} +class BadRequestError extends Error { name = "BadRequestError" } +class HTTPError extends Error { name = "HTTPError" } +class FailedYahooValidationError extends Error { name = "FailedYahooValidationError" } +class InvalidOptionsError extends Error { name = "InvalidOptionsError" } -module.exports = { BadRequestError, HTTPError }; +module.exports = { + BadRequestError, + HTTPError, + FailedYahooValidationError, + InvalidOptionsError, +}; diff --git a/src/lib/validate.ts b/src/lib/validate.ts index b4de5d88..a647d562 100644 --- a/src/lib/validate.ts +++ b/src/lib/validate.ts @@ -4,13 +4,18 @@ import addFormats from 'ajv-formats'; //import schema from '../../schema.json'; const schema = require('../../schema.json'); const pkg = require('../../package.json'); +import { InvalidOptionsError, FailedYahooValidationError } from './errors'; const ajv = new Ajv(); addFormats(ajv); ajv.addSchema(schema); -function validate(object: object, key: string): void { +const logObj = process.stdout.isTTY + ? (obj:any) => console.dir(obj, { depth: 4, colors: true }) + : (obj:any) => console.log(JSON.stringify(obj, null, 2)); + +function validate(object: object, key: string, module?: string): void { const validator = ajv.getSchema(key); if (!validator) throw new Error("No such schema with key: " + key); @@ -18,21 +23,31 @@ function validate(object: object, key: string): void { const valid = validator(object); if (valid) return; - const title = encodeURIComponent("Failed validation: " + key); - console.error("The following result did not validate with schema: " + key); - console.error(object); - console.error(validator.errors); - console.error("You should handle occassional errors in your code, however if "); - console.error("this happens every time, probably Yahoo have changed their API "); - console.error("and node-yahoo-finance2 needs to be updated. Please see if "); - console.error("anyone has reported this previously:"); - console.error(); - console.error(` ${pkg.repository}/issues?q=is%3Aissue+${title}`); - console.error(); - console.error("or open a new issue:"); - console.error(); - console.error(` ${pkg.repository}/issues/new?title=${title}`); - throw new Error("Failed Yahoo Schema validation"); + if (!module) { + + const title = encodeURIComponent("Failed validation: " + key); + console.error("The following result did not validate with schema: " + key); + logObj(object); + logObj(validator.errors); + console.error("You should handle occassional errors in your code, however if "); + console.error("this happens every time, probably Yahoo have changed their API "); + console.error("and node-yahoo-finance2 needs to be updated. Please see if "); + console.error("anyone has reported this previously:"); + console.error(); + console.error(` ${pkg.repository}/issues?q=is%3Aissue+${title}`); + console.error(); + console.error("or open a new issue:"); + console.error(); + console.error(` ${pkg.repository}/issues/new?title=${title}`); + throw new FailedYahooValidationError("Failed Yahoo Schema validation"); + + } else /* if (type === 'options') */ { + + console.error(`[yahooFinance.${module}] Invalid options ("${key}")`); + logObj({ input: object, errors: validator.errors }); + throw new InvalidOptionsError(`yahooFinance.${module} called with invalid options.`); + + } } export default validate; diff --git a/src/modules/autoc.spec.ts b/src/modules/autoc.spec.ts new file mode 100644 index 00000000..3efd601b --- /dev/null +++ b/src/modules/autoc.spec.ts @@ -0,0 +1,11 @@ +import autoc from './autoc'; +const { InvalidOptionsError } = require('../lib/errors'); + +describe('autoc', () => { + + it('throws InvalidOptions on invalid options', async () => { + const rwo = (options:any) => autoc('symbol', options); + await expect(rwo({ invalid: true })).rejects.toThrow(InvalidOptionsError) + }); + +}); diff --git a/src/modules/autoc.ts b/src/modules/autoc.ts index f69caf4b..c3337e9d 100644 --- a/src/modules/autoc.ts +++ b/src/modules/autoc.ts @@ -2,7 +2,8 @@ import yahooFinanceFetch from '../lib/yahooFinanceFetch'; import validate from '../lib/validate'; const QUERY_URL = 'https://autoc.finance.yahoo.com/autoc'; -const QUERY_SCHEMA_KEY = "#/definitions/YahooFinanceAutocResultSet"; +const QUERY_OPTIONS_SCHEMA_KEY = "#/definitions/AutocOptions" +const QUERY_RESULT_SCHEMA_KEY = "#/definitions/AutocResultSet"; export interface AutocResultSet { Query: string; @@ -18,7 +19,7 @@ export interface AutocResult { typeDisp: string; // "Equity" } -interface AutocOptions { +export interface AutocOptions { region?: number; // 1 lang?: string; // "en" } @@ -33,6 +34,8 @@ async function autoc( queryOptionsOverrides: AutocOptions = {}, fetchOptions?: object ): Promise { + validate(queryOptionsOverrides, QUERY_OPTIONS_SCHEMA_KEY, 'autoc'); + const queryOptions = { query, ...queryOptionsDefaults, @@ -42,7 +45,7 @@ async function autoc( const result = await yahooFinanceFetch(QUERY_URL, queryOptions, fetchOptions); if (result.ResultSet) { - validate(result.ResultSet, QUERY_SCHEMA_KEY); + validate(result.ResultSet, QUERY_RESULT_SCHEMA_KEY); return result.ResultSet; } diff --git a/src/modules/historical.spec.ts b/src/modules/historical.spec.ts new file mode 100644 index 00000000..28cae2ec --- /dev/null +++ b/src/modules/historical.spec.ts @@ -0,0 +1,11 @@ +import historical from './historical'; +const { InvalidOptionsError } = require('../lib/errors'); + +describe('historical', () => { + + it('throws InvalidOptions on invalid options', async () => { + const rwo = (options:any) => historical('symbol', options); + await expect(rwo({ invalid: true })).rejects.toThrow(InvalidOptionsError) + }); + +}); diff --git a/src/modules/historical.ts b/src/modules/historical.ts index bf146af4..6fb2f13c 100644 --- a/src/modules/historical.ts +++ b/src/modules/historical.ts @@ -3,7 +3,8 @@ import validate from '../lib/validate'; import csv2json from '../lib/csv2json'; const QUERY_URL = 'https://query1.finance.yahoo.com/v7/finance/download'; -const QUERY_SCHEMA_KEY = "#/definitions/HistoricalResult"; +const QUERY_RESULT_SCHEMA_KEY = "#/definitions/HistoricalResult"; +const QUERY_OPTIONS_SCHEMA_KEY = '#/definitions/HistoricalOptions'; export type HistoricalResult = Array; @@ -17,7 +18,7 @@ export interface HistoricalRow { volume: number; } -interface HistoricalOptions { +export interface HistoricalOptions { period1: Date | string | number; period2?: Date | string | number; interval?: '1d' | '1wk' | '1mo'; // '1d', TODO all | types @@ -36,6 +37,8 @@ export default async function historical( queryOptionsOverrides: HistoricalOptions, fetchOptions?: object ): Promise { + validate(queryOptionsOverrides, QUERY_OPTIONS_SCHEMA_KEY, 'historical'); + const queryOptions: HistoricalOptions = { ...queryOptionsDefaults, ...queryOptionsOverrides diff --git a/src/modules/quoteSummary.spec.ts b/src/modules/quoteSummary.spec.ts new file mode 100644 index 00000000..0af52789 --- /dev/null +++ b/src/modules/quoteSummary.spec.ts @@ -0,0 +1,11 @@ +import quoteSummary from './quoteSummary'; +const { InvalidOptionsError } = require('../lib/errors'); + +describe('quoteSummary', () => { + + it('throws InvalidOptions on invalid options', async () => { + const rwo = (options:any) => quoteSummary('symbol', options); + await expect(rwo({ invalid: true })).rejects.toThrow(InvalidOptionsError) + }); + +}); diff --git a/src/modules/quoteSummary.ts b/src/modules/quoteSummary.ts index 1a026cbd..3046a33d 100644 --- a/src/modules/quoteSummary.ts +++ b/src/modules/quoteSummary.ts @@ -4,7 +4,8 @@ import yahooFinanceFetch = require('../lib/yahooFinanceFetch'); import validate from '../lib/validate'; const QUERY_URL = 'https://query2.finance.yahoo.com/v10/finance/quoteSummary'; -const QUERY_SCHEMA_KEY = "#/definitions/QuoteSummaryResultOrig"; +const QUERY_OPTIONS_SCHEMA_KEY = '#/definitions/QuoteSummaryOptions' +const QUERY_RESULT_SCHEMA_KEY = "#/definitions/QuoteSummaryResultOrig"; /* const QUOTESUMMARY_MODULES = [ @@ -149,6 +150,8 @@ export default async function quoteSummary( queryOptionsOverrides: QuoteSummaryOptions = {}, fetchOptions?: object ): Promise { + validate(queryOptionsOverrides, QUERY_OPTIONS_SCHEMA_KEY, 'quoteSummary'); + const queryOptions = { ...queryOptionsDefaults, ...queryOptionsOverrides @@ -181,7 +184,7 @@ export default async function quoteSummary( const actualResult = qsResult.result[0]; - validate(actualResult, QUERY_SCHEMA_KEY); + validate(actualResult, QUERY_RESULT_SCHEMA_KEY); for (let key of DATEFIELDS) { const value:number|undefined = dotProp.get(actualResult, key); diff --git a/src/modules/search.spec.js b/src/modules/search.spec.js deleted file mode 100644 index ea657349..00000000 --- a/src/modules/search.spec.js +++ /dev/null @@ -1,7 +0,0 @@ -describe('seach', () => { - - it('passes :)', () => { - - }); - -}); diff --git a/src/modules/search.spec.ts b/src/modules/search.spec.ts new file mode 100644 index 00000000..09fbbd27 --- /dev/null +++ b/src/modules/search.spec.ts @@ -0,0 +1,11 @@ +import search from './search'; +const { InvalidOptionsError } = require('../lib/errors'); + +describe('search', () => { + + it('throws InvalidOptions on invalid options', async () => { + const rwo = (options:any) => search('symbol', options); + await expect(rwo({ invalid: true })).rejects.toThrow(InvalidOptionsError) + }); + +}); diff --git a/src/modules/search.ts b/src/modules/search.ts index 823a3f40..f6e3bda7 100644 --- a/src/modules/search.ts +++ b/src/modules/search.ts @@ -2,7 +2,8 @@ import yahooFinanceFetch = require('../lib/yahooFinanceFetch'); import validate from '../lib/validate'; const QUERY_URL = 'https://query2.finance.yahoo.com/v1/finance/search'; -const QUERY_SCHEMA_KEY = "#/definitions/SearchResultOrig"; +const QUERY_OPTIONS_SCHEMA_KEY = '#/definitions/SearchOptions'; +const QUERY_RESULT_SCHEMA_KEY = "#/definitions/SearchResultOrig"; export interface SearchQuoteYahoo { exchange: string; // "NYQ" @@ -57,7 +58,7 @@ export interface SearchResult extends Omit { news: Array; } -interface SearchOptions { +export interface SearchOptions { lang?: string; region?: string; quotesCount?: number; @@ -90,6 +91,8 @@ async function search( queryOptionsOverrides: SearchOptions = {}, fetchOptions?: object ): Promise { + validate(queryOptionsOverrides, QUERY_OPTIONS_SCHEMA_KEY, 'search'); + const queryOptions = { q: query, ...queryOptionsDefaults, @@ -97,7 +100,7 @@ async function search( }; const result = await yahooFinanceFetch(QUERY_URL, queryOptions, fetchOptions); - validate(result, QUERY_SCHEMA_KEY); + validate(result, QUERY_RESULT_SCHEMA_KEY); for (let news of result.news) news.providerPublishTime = new Date(news.providerPublishTime * 1000);