From ebccc84b50d112c5b0faf53139c77634e602cb1b Mon Sep 17 00:00:00 2001 From: odrail <> Date: Sat, 7 Dec 2024 14:53:14 +0100 Subject: [PATCH] Search quote API Fixes #4 --- README.md | 109 +++++++++-- index.d.ts | 19 ++ src/{ => api}/getHistoricalData.ts | 2 +- src/api/searchQuotes.ts | 47 +++++ src/index.ts | 6 +- .../mapGetHistoricalDataResponse.ts} | 0 test/getHistoricalData.spec.ts | 2 +- test/index.spec.ts | 181 +++++++++++++++++- ... => mapGetHistoricalDataResponse.spec .ts} | 2 +- 9 files changed, 341 insertions(+), 27 deletions(-) rename src/{ => api}/getHistoricalData.ts (94%) create mode 100644 src/api/searchQuotes.ts rename src/{mapResponse.ts => utils/mapGetHistoricalDataResponse.ts} (100%) rename test/{mapResponse.spec.ts => mapGetHistoricalDataResponse.spec .ts} (95%) diff --git a/README.md b/README.md index 3badf60..1f9c796 100644 --- a/README.md +++ b/README.md @@ -5,16 +5,33 @@ Unofficial APIs for Investing.com website. -### Install +## Install `npm i investing-com-api` -### Example +## APIs + +### getHistoricalData +Not all parameters are mandatory. + +- **input** String: input string, provide a valid investing.com pairId. (Required) +- **resolution** String: resolution of the response. + - Valid values: + - `5` (5 minutes) + - `60` (1 hour) + - `300` (5 hours) + - `D` (1 day, **Default**) + - `W` (week) + - `M` (month) +- **from** Date: a Date object to indicate the start of the period (Required) +- **to** Date: a Date object to indicate the end of the period (Required) + +#### Example ```js import { getHistoricalData } = from 'investing-com-api' async function main() { try { - const historicalData= await getHistoricalData({ + const historicalData = await getHistoricalData({ input: '46925', resolution: 'D', from: new Date('2024-10-15T00:00:00.000Z'), @@ -26,7 +43,7 @@ async function main() { } ``` -### Response +Response ```js [ { @@ -49,29 +66,79 @@ async function main() { ] ``` +### searchQuotes -### Inputs +Input params: +- **search** String (required) +- **options** Object (optional) + - **filters** Object (optional) + - **type** String (optional) + - **exchange** Array of Strings (optional) -#### getHistoricalData API -Not all parameters are mandatory. +Output: +```js +{ + pairId: number; + url: string; + description: string; + symbol: string; + exchange: string; + flag: string; + type: string; +}[] +``` -- **input** String: input string, provide a valid investing.com pairId. (Required) -- **resolution** String: resolution of the response. - - Valid values: - - `5` (5 minutes) - - `60` (1 hour) - - `300` (5 hours) - - `D` (1 day, **Default**) - - `W` (week) - - `M` (month) -- **from** Date: a Date object to indicate the start of the period (Required) -- **to** Date: a Date object to indicate the end of the period (Required) +#### Example +```js +import { searchQuotes } = from 'investing-com-api' + +async function main() { + try { + const quotes = await searchQuotes('SWDA') + } catch (err) { + console.error(err); + } +} +``` + +Response +```js +[ + { + pairId: 995448, + url: "/etfs/ishares-msci-world---acc?cid=995448", + description: "iShares Core MSCI World UCITS ETF USD (Acc)", + symbol: "SWDA", + exchange: "Switzerland", + flag: "Switzerland", + type: "ETF - Switzerland" + }, + { + pairId: 995447, + url: "/etfs/ishares-msci-world---acc?cid=995447", + description: "iShares Core MSCI World UCITS ETF USD (Acc)", + symbol: "SWDA", + exchange: "London", + flag: "UK", + type: "ETF - London" + }, + { + pairId: 46925, + url: "/etfs/ishares-msci-world---acc?cid=46925", + description: "iShares Core MSCI World UCITS ETF USD (Acc)", + symbol: "SWDA", + exchange: "Milan", + flag: "Italy", + type: "ETF - Milan" + } +] +``` -### Run tests +## Run tests `npm test` -### Run lint +## Run lint `npm run lint` -### Thanks to +## Thanks to - [Davide Violante](https://github.com/DavideViolante/) diff --git a/index.d.ts b/index.d.ts index 0b640e2..288b6d3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -38,4 +38,23 @@ declare module "investing-com-api" { export function getHistoricalData( params: GetHistoricalDataParams ): Promise; + + export type SearchQuoteOptions = { + filter?: { + type?: string + exchanges?: string[] + } + } + + export type SearchQuoteResponse = { + pairId: number; + url: string; + description: string; + symbol: string; + exchange: string; + flag: string; + type: string; + } + + export function searchQuotes(search: string, options?: SearchQuoteOptions): Promise } diff --git a/src/getHistoricalData.ts b/src/api/getHistoricalData.ts similarity index 94% rename from src/getHistoricalData.ts rename to src/api/getHistoricalData.ts index 9191fac..4343480 100644 --- a/src/getHistoricalData.ts +++ b/src/api/getHistoricalData.ts @@ -1,5 +1,5 @@ import { ChartResponse, GetHistoricalDataFn, GetHistoricalDataParams, GetHistoricalDataResponse, InvestmentData } from 'investing-com-api'; -import mapResponse from './mapResponse'; +import mapResponse from '../utils/mapGetHistoricalDataResponse'; const buildUrl = ({ input, resolution = 'D', from, to }: GetHistoricalDataParams): string => { const query = new URLSearchParams({ diff --git a/src/api/searchQuotes.ts b/src/api/searchQuotes.ts new file mode 100644 index 0000000..9a6f1da --- /dev/null +++ b/src/api/searchQuotes.ts @@ -0,0 +1,47 @@ +import { SearchQuoteOptions, SearchQuoteResponse } from "investing-com-api" + +type Quote = { + id: number; + url: string; + description: string; + symbol: string; + exchange: string; + flag: string; + type: string; +} + +type SearchDataResponse = { + quotes: Quote[] +} + +const mapQuote = (quote: Quote): SearchQuoteResponse => ({ + description: quote.description, + exchange: quote.exchange, + flag: quote.flag, + pairId: quote.id, + symbol: quote.symbol, + type: quote.type, + url: quote.url +}) + +const applyFilters = (quote: Quote, options?: SearchQuoteOptions): boolean => { + if (!options) return true + if (options.filter.type) { + if (!quote.type.includes(options.filter.type)) return false + } + if (options.filter.exchanges) { + if (!options.filter.exchanges.includes(quote.exchange)) return false + } + return true +} + +const searchQuotes = async (search: string, options?: SearchQuoteOptions): Promise => { + const response = await fetch(`https://api.investing.com/api/search/v2/search?q=${search}`) + if (!response.ok) return Promise.reject(`Response status: ${response.status}`); + const body = await response.json() as SearchDataResponse + return body.quotes + .filter(quote => applyFilters(quote, options)) + .map(mapQuote) +} + +export default searchQuotes \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index ac01b11..060ec03 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ -import getHistoricalData from "./getHistoricalData"; +import getHistoricalData from "./api/getHistoricalData"; +import searchQuotes from "./api/searchQuotes"; export { - getHistoricalData + getHistoricalData, + searchQuotes } \ No newline at end of file diff --git a/src/mapResponse.ts b/src/utils/mapGetHistoricalDataResponse.ts similarity index 100% rename from src/mapResponse.ts rename to src/utils/mapGetHistoricalDataResponse.ts diff --git a/test/getHistoricalData.spec.ts b/test/getHistoricalData.spec.ts index eba1625..34ac495 100644 --- a/test/getHistoricalData.spec.ts +++ b/test/getHistoricalData.spec.ts @@ -1,4 +1,4 @@ -import getHistoricalData from "../src/getHistoricalData"; +import getHistoricalData from "../src/api/getHistoricalData"; import nock from 'nock' describe('Tests for getHistoricalData()', () => { diff --git a/test/index.spec.ts b/test/index.spec.ts index fbb981b..f843dcf 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,4 +1,5 @@ -import { getHistoricalData } from '../src/index'; +import nock from 'nock' +import { getHistoricalData, searchQuotes } from '../src/index'; describe('Tests for Investing.com unofficial APIs', () => { describe('getHistorical API', () => { @@ -6,5 +7,183 @@ describe('Tests for Investing.com unofficial APIs', () => { expect(getHistoricalData).toBeInstanceOf(Function) }) }) + + describe('searchQuotes API', () => { + const scope = nock('https://api.investing.com'); + const investingSearchResponse = { + quotes: [ + { + id: 1, + url: "/fake-path?cid=1", + description: "fake description", + symbol: "fake-symbol", + exchange: "Switzerland", + flag: "Switzerland", + type: "ETF - Switzerland" + }, + { + id: 2, + url: "/fake-path?cid=2", + description: "fake description", + symbol: "fake-symbol", + exchange: "London", + flag: "UK", + type: "ETF - London" + }, + { + id: 3, + url: "/fake-path?cid=3", + description: "fake description", + symbol: "fake-symbol", + exchange: "Milan", + flag: "Italy", + type: "ETF - Milan" + }, + { + id: 4, + url: "/fake-path?cid=4", + description: "fake description", + symbol: "fake-symbol", + exchange: "Milan", + flag: "Italy", + type: "Index - Milan" + } + ] + } + it('should return a function', async () => { + expect(searchQuotes).toBeInstanceOf(Function) + }) + + it('should reject promise if api response with an error', () => { + const search = 'fake-symbol' + scope + .get(`/api/search/v2/search?q=${search}`) + .reply(500) + expect(searchQuotes(search)).rejects.toBe("Response status: 500") + }) + + it('should return all quotes', async () => { + const search = 'fake-symbol' + scope + .get(`/api/search/v2/search?q=${search}`) + .reply(200, investingSearchResponse) + const response = await searchQuotes(search) + const expected = [ + { + pairId: 1, + url: "/fake-path?cid=1", + description: "fake description", + symbol: "fake-symbol", + exchange: "Switzerland", + flag: "Switzerland", + type: "ETF - Switzerland" + }, + { + pairId: 2, + url: "/fake-path?cid=2", + description: "fake description", + symbol: "fake-symbol", + exchange: "London", + flag: "UK", + type: "ETF - London" + }, + { + pairId: 3, + url: "/fake-path?cid=3", + description: "fake description", + symbol: "fake-symbol", + exchange: "Milan", + flag: "Italy", + type: "ETF - Milan" + }, + { + pairId: 4, + url: "/fake-path?cid=4", + description: "fake description", + symbol: "fake-symbol", + exchange: "Milan", + flag: "Italy", + type: "Index - Milan" + } + ] + expect(response).toEqual(expected) + }) + + it('should return quotes filtered by exchange', async () => { + const search = 'fake-symbol' + scope + .get(`/api/search/v2/search?q=${search}`) + .reply(200, investingSearchResponse) + const response = await searchQuotes(search, { filter: {exchanges: ['Milan', 'London']}}) + const expected = [ + { + pairId: 2, + url: "/fake-path?cid=2", + description: "fake description", + symbol: "fake-symbol", + exchange: "London", + flag: "UK", + type: "ETF - London" + }, + { + pairId: 3, + url: "/fake-path?cid=3", + description: "fake description", + symbol: "fake-symbol", + exchange: "Milan", + flag: "Italy", + type: "ETF - Milan" + }, + { + pairId: 4, + url: "/fake-path?cid=4", + description: "fake description", + symbol: "fake-symbol", + exchange: "Milan", + flag: "Italy", + type: "Index - Milan" + } + ] + expect(response).toEqual(expected) + }) + + it('should return quotes filtered by type', async () => { + const search = 'fake-symbol' + scope + .get(`/api/search/v2/search?q=${search}`) + .reply(200, investingSearchResponse) + const response = await searchQuotes(search, { filter: {type: 'ETF'}}) + const expected = [ + { + pairId: 1, + url: "/fake-path?cid=1", + description: "fake description", + symbol: "fake-symbol", + exchange: "Switzerland", + flag: "Switzerland", + type: "ETF - Switzerland" + }, + { + pairId: 2, + url: "/fake-path?cid=2", + description: "fake description", + symbol: "fake-symbol", + exchange: "London", + flag: "UK", + type: "ETF - London" + }, + { + pairId: 3, + url: "/fake-path?cid=3", + description: "fake description", + symbol: "fake-symbol", + exchange: "Milan", + flag: "Italy", + type: "ETF - Milan" + } + ] + expect(response).toEqual(expected) + }) + }) }); diff --git a/test/mapResponse.spec.ts b/test/mapGetHistoricalDataResponse.spec .ts similarity index 95% rename from test/mapResponse.spec.ts rename to test/mapGetHistoricalDataResponse.spec .ts index 14a5618..7461360 100644 --- a/test/mapResponse.spec.ts +++ b/test/mapGetHistoricalDataResponse.spec .ts @@ -1,5 +1,5 @@ import { ChartResponse } from "investing-com-api"; -import mapResponse from "../src/mapResponse"; +import mapResponse from "../src/utils/mapGetHistoricalDataResponse"; describe('TEST mapResponse', () => { const mockData: ChartResponse[] = [