From 37c3c75486522d4f8541876415e73ced2afa1bf6 Mon Sep 17 00:00:00 2001 From: Amaury <1293565+amaurym@users.noreply.github.com> Date: Sat, 18 Feb 2023 16:03:29 +0100 Subject: [PATCH 1/2] refactor!: Remove fp-ts (#729) * refactor!: Remove fp-ts * Fix all warnings * Fix tests * Fix lint * Fix tests --- .env.example | 2 +- packages/dataproviders/package.json | 4 +- packages/dataproviders/src/fp/index.ts | 1 - packages/dataproviders/src/index.ts | 8 +- packages/dataproviders/src/promise.ts | 50 ---- .../src/providers/aqicn/aqicn.spec.ts | 6 +- .../src/providers/aqicn/aqicn.ts | 9 +- .../src/providers/aqicn/fetchBy.ts | 73 ++---- .../src/providers/aqicn/normalize.ts | 123 +++++----- .../src/providers/aqicn/validation.ts | 90 +++---- .../src/providers/openaq/fetchBy.ts | 27 +-- .../src/providers/openaq/normalize.ts | 24 +- .../src/providers/openaq/openaq.ts | 13 +- .../src/providers/openaq/types.ts | 18 ++ .../src/providers/openaq/validation.ts | 29 --- packages/dataproviders/src/providers/types.ts | 17 +- .../src/providers/waqi/fetchBy.ts | 11 +- .../src/providers/waqi/normalize.ts | 93 ++++---- .../waqi/{validation.ts => types.ts} | 36 ++- .../src/providers/waqi/waqi.spec.ts | 10 +- .../dataproviders/src/providers/waqi/waqi.ts | 14 +- packages/dataproviders/src/util/codec.spec.ts | 16 -- packages/dataproviders/src/util/codec.ts | 61 ----- packages/dataproviders/src/util/constants.ts | 5 - .../src/util/countryCode.spec.ts | 14 +- .../dataproviders/src/util/countryCode.ts | 30 +-- packages/dataproviders/src/util/fetch.ts | 19 ++ packages/dataproviders/src/util/fp.spec.ts | 26 -- packages/dataproviders/src/util/fp.ts | 63 ----- packages/dataproviders/src/util/index.ts | 2 - packages/dataproviders/src/util/openaq.ts | 178 ++++---------- packages/dataproviders/test/e2e/aqicn.spec.ts | 20 +- .../dataproviders/test/e2e/openaq.spec.ts | 85 +++---- .../dataproviders/test/e2e/promise.spec.ts | 4 +- packages/dataproviders/test/util/testUtil.ts | 96 ++++---- packages/ui/package.json | 7 +- .../ui/src/components/BoxButton/BoxButton.tsx | 76 ------ packages/ui/src/components/BoxButton/index.ts | 17 -- .../Cigarettes/Cigarette/Cigarette.tsx | 225 ------------------ .../components/Cigarettes/Cigarette/index.ts | 17 -- .../src/components/Cigarettes/Cigarettes.tsx | 140 ----------- .../ui/src/components/Cigarettes/index.ts | 17 -- packages/ui/src/components/index.ts | 18 -- packages/ui/src/util/api.ts | 6 +- packages/ui/src/util/fp.ts | 101 -------- packages/ui/src/util/geoapify.ts | 78 ++---- packages/ui/src/util/index.ts | 1 - packages/ui/src/util/provider.ts | 11 +- packages/ui/src/util/testutil.ts | 4 +- packages/ui/test/e2e/geoapify.spec.ts | 23 +- yarn.lock | 111 +-------- 51 files changed, 444 insertions(+), 1685 deletions(-) delete mode 100644 packages/dataproviders/src/fp/index.ts delete mode 100644 packages/dataproviders/src/promise.ts create mode 100644 packages/dataproviders/src/providers/openaq/types.ts delete mode 100644 packages/dataproviders/src/providers/openaq/validation.ts rename packages/dataproviders/src/providers/waqi/{validation.ts => types.ts} (57%) delete mode 100644 packages/dataproviders/src/util/codec.spec.ts delete mode 100644 packages/dataproviders/src/util/codec.ts create mode 100644 packages/dataproviders/src/util/fetch.ts delete mode 100644 packages/dataproviders/src/util/fp.spec.ts delete mode 100644 packages/dataproviders/src/util/fp.ts delete mode 100644 packages/ui/src/components/BoxButton/BoxButton.tsx delete mode 100644 packages/ui/src/components/BoxButton/index.ts delete mode 100644 packages/ui/src/components/Cigarettes/Cigarette/Cigarette.tsx delete mode 100644 packages/ui/src/components/Cigarettes/Cigarette/index.ts delete mode 100644 packages/ui/src/components/Cigarettes/Cigarettes.tsx delete mode 100644 packages/ui/src/components/Cigarettes/index.ts delete mode 100644 packages/ui/src/components/index.ts delete mode 100644 packages/ui/src/util/fp.ts diff --git a/.env.example b/.env.example index 875dfdeb..21eb0a4f 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -AQICN_TOKEN= +AQICN_TOKEN=6bb4237574756ba29f05cea553bd22576596c11e diff --git a/packages/dataproviders/package.json b/packages/dataproviders/package.json index 0da6558a..826638ef 100644 --- a/packages/dataproviders/package.json +++ b/packages/dataproviders/package.json @@ -21,9 +21,7 @@ "@shootismoke/convert": "^0.9.1", "axios": "^0.27.2", "date-fns": "^2.29.3", - "date-fns-tz": "^1.3.7", - "fp-ts": "^2.12.3", - "io-ts": "^2.2.18" + "date-fns-tz": "^1.3.7" }, "devDependencies": { "dotenv": "^16.0.3" diff --git a/packages/dataproviders/src/fp/index.ts b/packages/dataproviders/src/fp/index.ts deleted file mode 100644 index a14d0885..00000000 --- a/packages/dataproviders/src/fp/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '../providers'; diff --git a/packages/dataproviders/src/index.ts b/packages/dataproviders/src/index.ts index 03f3e705..fbc11ff3 100644 --- a/packages/dataproviders/src/index.ts +++ b/packages/dataproviders/src/index.ts @@ -1,13 +1,15 @@ +export * from './providers/types'; export { AqicnOptions } from './providers/aqicn/fetchBy'; export { OpenAQOptions } from './providers/openaq/fetchBy'; -export * from './promise'; export * from './types'; export { - AllProviders, ACCURATE_RADIUS, getDominantPol, getCountryFromCode, stationName, } from './util'; -export * from './util/fp'; export * from './util/openaq'; + +export * from './providers/aqicn/aqicn'; +export * from './providers/openaq/openaq'; +export * from './providers/waqi/waqi'; diff --git a/packages/dataproviders/src/promise.ts b/packages/dataproviders/src/promise.ts deleted file mode 100644 index 3d041717..00000000 --- a/packages/dataproviders/src/promise.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - aqicn as aqicnFp, - openaq as openaqFp, - waqi as waqiFp, -} from './providers'; -import { ProviderFP } from './providers/types'; -import { LatLng, OpenAQResults } from './types'; -import { eitherToFunction, teToPromise } from './util'; - -/** - * An interface representing an air quality data Provider (Promise version). - */ -export interface Provider { - fetchByGps(gps: LatLng, options?: Options): Promise; - fetchByStation( - stationId: string, - options?: Options - ): Promise; - id: string; - name: string; - normalizeByGps(d: DataByGps): OpenAQResults; - normalizeByStation(d: DataByStation): OpenAQResults; -} - -function promisifyProviderFP( - ProviderFP: ProviderFP -): Provider { - return { - ...ProviderFP, - fetchByGps(gps: LatLng, options?: Options): Promise { - return teToPromise(ProviderFP.fetchByGps(gps, options)); - }, - fetchByStation( - stationId: string, - options?: Options - ): Promise { - return teToPromise(ProviderFP.fetchByStation(stationId, options)); - }, - normalizeByGps(d: DataByGps): OpenAQResults { - return eitherToFunction(ProviderFP.normalizeByGps(d)); - }, - normalizeByStation(d: DataByStation): OpenAQResults { - return eitherToFunction(ProviderFP.normalizeByStation(d)); - }, - }; -} - -export const aqicn = promisifyProviderFP(aqicnFp); -export const openaq = promisifyProviderFP(openaqFp); -export const waqi = promisifyProviderFP(waqiFp); diff --git a/packages/dataproviders/src/providers/aqicn/aqicn.spec.ts b/packages/dataproviders/src/providers/aqicn/aqicn.spec.ts index d61daf6d..0269d5a5 100644 --- a/packages/dataproviders/src/providers/aqicn/aqicn.spec.ts +++ b/packages/dataproviders/src/providers/aqicn/aqicn.spec.ts @@ -1,11 +1,9 @@ -import * as E from 'fp-ts/lib/Either'; - import { aqicn } from './aqicn'; describe('aqicn', () => { it('should throw without token', async () => { - expect(await aqicn.fetchByStation('foo')()).toEqual( - E.left(new Error('AqiCN requires a token')) + await expect(aqicn.fetchByStation('foo')).rejects.toThrowError( + new Error('AqiCN requires a token') ); }); }); diff --git a/packages/dataproviders/src/providers/aqicn/aqicn.ts b/packages/dataproviders/src/providers/aqicn/aqicn.ts index c7d6113d..a9eb5420 100644 --- a/packages/dataproviders/src/providers/aqicn/aqicn.ts +++ b/packages/dataproviders/src/providers/aqicn/aqicn.ts @@ -1,16 +1,15 @@ -import { ProviderFP } from '../types'; +import { Provider } from '../types'; import { AqicnOptions, fetchByGps, fetchByStation } from './fetchBy'; import { normalize } from './normalize'; -import { AqicnStaton } from './validation'; +import { AqicnData } from './validation'; /** * @see https://aqicn.org */ -export const aqicn: ProviderFP = { +export const aqicn: Provider = { fetchByGps, fetchByStation, id: 'aqicn', name: 'AQI CN', - normalizeByGps: normalize, - normalizeByStation: normalize, + normalize, }; diff --git a/packages/dataproviders/src/providers/aqicn/fetchBy.ts b/packages/dataproviders/src/providers/aqicn/fetchBy.ts index 0d8e4c17..8ea9059e 100644 --- a/packages/dataproviders/src/providers/aqicn/fetchBy.ts +++ b/packages/dataproviders/src/providers/aqicn/fetchBy.ts @@ -1,22 +1,17 @@ -import { pipe } from 'fp-ts/lib/pipeable'; -import * as TE from 'fp-ts/lib/TaskEither'; - import { LatLng } from '../../types'; -import { fetchAndDecode } from '../../util'; -import { AqicnStaton, ByStation, ByStationCodec } from './validation'; +import { fetchAndDecode } from '../../util/fetch'; +import { AqicnData, AqicnResponse } from './validation'; /** * Check if the response we get from aqicn is `{"status": "error", "msg": "..."}`, * if yes, return an error. */ -function checkError({ - status, - data, - msg, -}: ByStation): TE.TaskEither { - return status === 'ok' - ? TE.right(data) - : TE.left(new Error(msg || (data as string))); +function checkError({ status, data, msg }: AqicnResponse): AqicnData { + if (status === 'ok' && typeof data === 'object') { + return data; + } else { + throw new Error(msg || (data as string)); + } } export interface AqicnOptions { @@ -27,18 +22,6 @@ export interface AqicnOptions { token: string; } -/** - * Check that a token has been correctly passed - * - * @param options - Options to pass to aqicn - */ -function checkToken(options?: AqicnOptions): TE.TaskEither { - if (!options || !options.token) { - return TE.left(new Error('AqiCN requires a token')); - } - return TE.right(undefined); -} - /** * Fetch the closest station to the user's current position * @@ -47,19 +30,16 @@ function checkToken(options?: AqicnOptions): TE.TaskEither { export function fetchByGps( gps: LatLng, options: AqicnOptions -): TE.TaskEither { +): Promise { + if (!options || !options.token) { + throw new Error('AqiCN requires a token'); + } + const { latitude, longitude } = gps; - return pipe( - checkToken(options), - TE.chain(() => - fetchAndDecode( - `https://api.waqi.info/feed/geo:${latitude};${longitude}/?token=${options.token}`, - ByStationCodec - ) - ), - TE.chain(checkError) - ); + return fetchAndDecode( + `https://api.waqi.info/feed/geo:${latitude};${longitude}/?token=${options.token}` + ).then(checkError); } /** @@ -67,18 +47,15 @@ export function fetchByGps( * * @param stationId - The station ID to search */ -export function fetchByStation( +export async function fetchByStation( stationId: string, options: AqicnOptions -): TE.TaskEither { - return pipe( - checkToken(options), - TE.chain(() => - fetchAndDecode( - `https://api.waqi.info/feed/@${stationId}/?token=${options.token}`, - ByStationCodec - ) - ), - TE.chain(checkError) - ); +): Promise { + if (!options || !options.token) { + throw new Error('AqiCN requires a token'); + } + + return fetchAndDecode( + `https://api.waqi.info/feed/@${stationId}/?token=${options.token}` + ).then(checkError); } diff --git a/packages/dataproviders/src/providers/aqicn/normalize.ts b/packages/dataproviders/src/providers/aqicn/normalize.ts index e2441852..98a77155 100644 --- a/packages/dataproviders/src/providers/aqicn/normalize.ts +++ b/packages/dataproviders/src/providers/aqicn/normalize.ts @@ -6,13 +6,11 @@ import { usaEpa, } from '@shootismoke/convert'; import { format, utcToZonedTime } from 'date-fns-tz'; -import * as E from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; import { OpenAQResults } from '../../types'; import { getCountryCode, providerError } from '../../util'; import sanitized from './sanitized.json'; -import type { AqicnStaton } from './validation'; +import type { AqicnData } from './validation'; /** * Sanitize the country we get here from aqicn. For example, for China, the @@ -32,28 +30,20 @@ export function sanitizeCountry(input: string): string { } /** - * Normalize aqicn byGps data + * Normalize aqicn byGps data. Throws an error if the data cannot be normalized. * * @param data - The data to normalize */ -export function normalize(data: AqicnStaton): E.Either { - if (!data || typeof data === 'string') { - return E.left( - providerError('aqicn', `Cannot normalized ${data || 'undefined'}`) - ); - } - +export function normalize(data: AqicnData): OpenAQResults { const stationId = `aqicn|${data.idx}`; // Sometimes we don't get geo if (!data.city.geo) { - return E.left( - providerError( - 'aqicn', - `Cannot normalize station ${stationId}, no city: ${JSON.stringify( - data - )}` - ) + throw providerError( + 'aqicn', + `Cannot normalize station ${stationId}, no city: ${JSON.stringify( + data + )}` ); } @@ -63,13 +53,11 @@ export function normalize(data: AqicnStaton): E.Either { usaEpa.pollutants.includes(pol as Pollutant) ); if (!pollutants.length) { - return E.left( - providerError( - 'aqicn', - `Cannot normalize station ${stationId}, no pollutants currently tracked: ${JSON.stringify( - data - )}` - ) + throw providerError( + 'aqicn', + `Cannot normalize station ${stationId}, no pollutants currently tracked: ${JSON.stringify( + data + )}` ); } // We now need to get the country from AQICN response. The only place I found @@ -77,13 +65,9 @@ export function normalize(data: AqicnStaton): E.Either { // Example: https://aqicn.org/city/france/lorraine/thionville-nord/garche/ const AQICN_DOMAIN = 'aqicn.org/city/'; if (!data.city.url || !data.city.url.includes(AQICN_DOMAIN)) { - return E.left( - providerError( - 'aqicn', - `Cannot extract country, got city.url: ${ - data.city.url as string - }` - ) + throw providerError( + 'aqicn', + `Cannot extract country, got city.url: ${data.city.url as string}` ); } const countryRaw = sanitizeCountry( @@ -99,44 +83,43 @@ export function normalize(data: AqicnStaton): E.Either { "yyyy-MM-dd'T'HH:mm:ss.SSSxxx" ); - return pipe( - getCountryCode(countryRaw), - E.fromOption(() => - providerError('aqicn', `Cannot get code from country ${countryRaw}`) - ), - E.map( - (country) => - pollutants.map(([pol, { v }]) => { - const pollutant = pol as Pollutant; + const countryCode = getCountryCode(countryRaw); + if (!countryCode) { + throw providerError( + 'aqicn', + `Cannot get code from country ${countryRaw}` + ); + } - if (!data.city.geo) { - throw new Error( - 'We returned TE.left if data.city.geo was not defined. qed.' - ); - } + return pollutants.map(([pol, { v }]) => { + const pollutant = pol as Pollutant; - return { - attribution: data.attributions, - averagingPeriod: { - unit: 'day', - value: 1, - }, - city: data.city.name, - coordinates: { - latitude: +data.city.geo[0], - longitude: +data.city.geo[1], - }, - country, - date: { local, utc }, - location: stationId, - isMobile: false, - parameter: pollutant, - sourceName: 'aqicn', - entity: 'other', - value: convert(pollutant, 'usaEpa', ugm3, v), - unit: getPollutantMeta(pollutant).preferredUnit, - }; - }) as OpenAQResults - ) - ); + if (!data.city.geo) { + throw new Error( + 'We returned TE.left if data.city.geo was not defined. qed.' + ); + } + + return { + attribution: data.attributions, + averagingPeriod: { + unit: 'day', + value: 1, + }, + city: data.city.name, + coordinates: { + latitude: +data.city.geo[0], + longitude: +data.city.geo[1], + }, + country: countryCode, + date: { local, utc }, + location: stationId, + isMobile: false, + parameter: pollutant, + sourceName: 'aqicn', + entity: 'other', + value: convert(pollutant, 'usaEpa', ugm3, v), + unit: getPollutantMeta(pollutant).preferredUnit, + }; + }) as OpenAQResults; } diff --git a/packages/dataproviders/src/providers/aqicn/validation.ts b/packages/dataproviders/src/providers/aqicn/validation.ts index 608bdb70..b16c900f 100644 --- a/packages/dataproviders/src/providers/aqicn/validation.ts +++ b/packages/dataproviders/src/providers/aqicn/validation.ts @@ -1,7 +1,3 @@ -import * as t from 'io-ts'; - -import { attributionsCodec } from '../../util'; - // Example response // Object { // "data": Object { @@ -58,61 +54,33 @@ import { attributionsCodec } from '../../util'; // "status": "ok", // } -const pollutantValue = t.type({ - v: t.number, -}); - -// Ideally, we should have a list of all pollutants tracked by AqiCN, but I -// didn't finy any. Putting string for now. -const pollutants = t.record(t.string, pollutantValue); - -const AqicnStationCodecData = t.union([ - t.type({ - attributions: attributionsCodec, - city: t.type({ - geo: t.union([ - t.tuple([ - // Somehow, we also sometimes get strings as geo lat/lng - t.union([t.string, t.number]), - t.union([t.string, t.number]), - ]), - // We also could get null - t.null, - ]), - name: t.union([t.string, t.undefined]), - url: t.union([t.string, t.undefined]), - }), - // Should be `t.keyof(pollutants.props)`, but sometimes we do get "" or undefined - dominentpol: t.union([t.string, t.undefined]), - // All pollutants - iaqi: t.union([pollutants, t.undefined]), - // Station ID - idx: t.number, - time: t.type({ - // As string - s: t.union([t.string, t.undefined]), - // Timezone - tz: t.union([t.string, t.undefined]), - // As timestamp - v: t.number, - // As ISO string - iso: t.union([t.undefined, t.string]), - }), - }), - t.string, - t.undefined, -]); - -export const ByStationCodec = t.type({ - status: t.keyof({ - ok: null, - error: null, - nope: null, // http://api.waqi.info/feed/geo:31.54;84.3/?token= - }), - data: AqicnStationCodecData, - msg: t.union([t.string, t.undefined]), -}); - -export type ByStation = t.TypeOf; +export type AqicnData = { + attributions?: { + name?: string; + url?: string; + }[]; + city: { + geo?: [string | number, string | number]; + name?: string; + url?: string; + }; + dominentpol?: string; + iaqi?: { + [key: string]: { + v: number; + }; + }; + idx: number; + time: { + s?: string; + tz?: string; + v: number; + iso?: string; + }; +}; -export type AqicnStaton = t.TypeOf; +export type AqicnResponse = { + data?: AqicnData | string; + msg?: string; + status: 'ok' | 'error' | 'nope'; +}; diff --git a/packages/dataproviders/src/providers/openaq/fetchBy.ts b/packages/dataproviders/src/providers/openaq/fetchBy.ts index 6aa5116d..5919d840 100644 --- a/packages/dataproviders/src/providers/openaq/fetchBy.ts +++ b/packages/dataproviders/src/providers/openaq/fetchBy.ts @@ -1,15 +1,10 @@ import { Pollutant } from '@shootismoke/convert'; import type { AxiosError } from 'axios'; -import * as TE from 'fp-ts/lib/TaskEither'; import { LatLng } from '../../types'; -import { - ACCURATE_RADIUS, - fetchAndDecode, - OpenAQError, - OpenAQErrorObject, -} from '../../util'; -import { OpenAQMeasurements, OpenAQMeasurementsCodec } from './validation'; +import { ACCURATE_RADIUS, OpenAQError, OpenAQErrorObject } from '../../util'; +import { fetchAndDecode } from '../../util/fetch'; +import { OpenAQMeasurements } from './types'; const RESULT_LIMIT = 10; const OPENAQ_MEASUREMENTS_V2 = `https://api.openaq.org/v2/measurements`; @@ -73,7 +68,7 @@ function additionalOptions(options: OpenAQOptions = {}): string { /** * Handle error from OpenAQ response */ -function onError(err: AxiosError): Error { +function formatError(err: AxiosError): Error { // We had occasions from OpenAQ where the error had an empty response field // so we check that the data is populated first. if (err?.response?.data) { @@ -99,7 +94,7 @@ function onError(err: AxiosError): Error { export function fetchByGps( gps: LatLng, options?: OpenAQOptions -): TE.TaskEither { +): Promise { // OpenAQ doesn't allow arbitrary number of decimals, we round to 3. const latitude = Math.round(gps.latitude * 1000) / 1000; const longitude = Math.round(gps.longitude * 1000) / 1000; @@ -108,10 +103,7 @@ export function fetchByGps( `${OPENAQ_MEASUREMENTS_V2}?coordinates=${latitude},${longitude}&radius=${ACCURATE_RADIUS}${additionalOptions( options )}`, - OpenAQMeasurementsCodec, - { - onError, - } + { formatError } ); } @@ -123,14 +115,11 @@ export function fetchByGps( export function fetchByStation( stationId: string, options?: OpenAQOptions -): TE.TaskEither { +): Promise { return fetchAndDecode( `${OPENAQ_MEASUREMENTS_V2}?location=${stationId}${additionalOptions( options )}`, - OpenAQMeasurementsCodec, - { - onError, - } + { formatError } ); } diff --git a/packages/dataproviders/src/providers/openaq/normalize.ts b/packages/dataproviders/src/providers/openaq/normalize.ts index 8d6b2d7f..300d36c2 100644 --- a/packages/dataproviders/src/providers/openaq/normalize.ts +++ b/packages/dataproviders/src/providers/openaq/normalize.ts @@ -1,29 +1,21 @@ -import * as E from 'fp-ts/lib/Either'; - import { OpenAQResults } from '../../types'; import { providerError } from '../../util'; -import { OpenAQMeasurements } from './validation'; +import { OpenAQMeasurements } from './types'; /** - * Normalize aqicn byGps data + * Normalize aqicn byGps data. Throws an error if the data cannot be normalized. * * @param data - The data to normalize */ -export function normalize( - data: OpenAQMeasurements -): E.Either { +export function normalize(data: OpenAQMeasurements): OpenAQResults { const { results } = data; if (!results.length) { - return E.left( - providerError('openaq', 'Cannot normalize, got 0 result') - ); + throw providerError('openaq', 'Cannot normalize, got 0 result'); } - return E.right( - results.map((result) => ({ - ...result, - location: `openaq|${result.location}`, - })) as OpenAQResults - ); + return results.map((result) => ({ + ...result, + location: `openaq|${result.location}`, + })) as OpenAQResults; } diff --git a/packages/dataproviders/src/providers/openaq/openaq.ts b/packages/dataproviders/src/providers/openaq/openaq.ts index 7dcaf008..b90a9ca1 100644 --- a/packages/dataproviders/src/providers/openaq/openaq.ts +++ b/packages/dataproviders/src/providers/openaq/openaq.ts @@ -1,17 +1,12 @@ -import { ProviderFP } from '../types'; +import { Provider } from '../types'; import { fetchByGps, fetchByStation, OpenAQOptions } from './fetchBy'; import { normalize } from './normalize'; -import { OpenAQMeasurements } from './validation'; +import { OpenAQMeasurements } from './types'; -export const openaq: ProviderFP< - OpenAQMeasurements, - OpenAQMeasurements, - OpenAQOptions -> = { +export const openaq: Provider = { fetchByGps, fetchByStation, id: 'openaq', name: 'Open AQ', - normalizeByGps: normalize, - normalizeByStation: normalize, + normalize, }; diff --git a/packages/dataproviders/src/providers/openaq/types.ts b/packages/dataproviders/src/providers/openaq/types.ts new file mode 100644 index 00000000..d5c362b2 --- /dev/null +++ b/packages/dataproviders/src/providers/openaq/types.ts @@ -0,0 +1,18 @@ +import type { OpenAQResult } from '../../util'; + +/** + * Return type for the /v2/measurements endpoint. + * + * @see https://docs.openaq.org/#/v2/measurements_get_v2_measurements_get + */ +export type OpenAQMeasurements = { + meta: { + found: number; + license: string; + limit: number; + name: string; + page: number; + website: string; + }; + results: OpenAQResult[]; +}; diff --git a/packages/dataproviders/src/providers/openaq/validation.ts b/packages/dataproviders/src/providers/openaq/validation.ts deleted file mode 100644 index be62dd5e..00000000 --- a/packages/dataproviders/src/providers/openaq/validation.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as t from 'io-ts'; - -import { OpenAQCodec } from '../../util'; - -const OpenAQMetaCodec = t.type({ - found: t.number, - license: t.string, - limit: t.number, - name: t.string, - page: t.number, - website: t.string, -}); - -/** - * Codec for the /v2/measurements endpoint. - * - * @see https://docs.openaq.org/#/v2/measurements_get_v2_measurements_get - */ -export const OpenAQMeasurementsCodec = t.type({ - meta: OpenAQMetaCodec, - results: t.array(OpenAQCodec), -}); - -/** - * Return type for the /v2/measurements endpoint. - * - * @see https://docs.openaq.org/#/v2/measurements_get_v2_measurements_get - */ -export type OpenAQMeasurements = t.TypeOf; diff --git a/packages/dataproviders/src/providers/types.ts b/packages/dataproviders/src/providers/types.ts index f52afa41..7c2026b4 100644 --- a/packages/dataproviders/src/providers/types.ts +++ b/packages/dataproviders/src/providers/types.ts @@ -1,19 +1,12 @@ -import * as E from 'fp-ts/lib/Either'; -import * as TE from 'fp-ts/lib/TaskEither'; - import type { LatLng, OpenAQResults } from '../types'; /** - * An interface representing an air quality data provider (fp-ts version). + * An interface representing an air quality data provider. */ -export interface ProviderFP { - fetchByGps(gps: LatLng, options?: Options): TE.TaskEither; - fetchByStation( - stationId: string, - options?: Options - ): TE.TaskEither; +export interface Provider { + fetchByGps(gps: LatLng, options?: Options): Promise; + fetchByStation(stationId: string, options?: Options): Promise; id: string; name: string; - normalizeByGps(d: DataByGps): E.Either; - normalizeByStation(d: DataByStation): E.Either; + normalize: (d: Response) => OpenAQResults; } diff --git a/packages/dataproviders/src/providers/waqi/fetchBy.ts b/packages/dataproviders/src/providers/waqi/fetchBy.ts index 32823f06..61078172 100644 --- a/packages/dataproviders/src/providers/waqi/fetchBy.ts +++ b/packages/dataproviders/src/providers/waqi/fetchBy.ts @@ -1,19 +1,16 @@ -import * as TE from 'fp-ts/lib/TaskEither'; - import { LatLng } from '../../types'; -import { fetchAndDecode } from '../../util'; -import { ByStation, ByStationCodec } from './validation'; +import { fetchAndDecode } from '../../util/fetch'; +import { WaqiResponse } from './types'; /** * Fetch the closest station to the user's current position. * * @param gps - Latitude and longitude of the user's current position */ -export function fetchByGps(gps: LatLng): TE.TaskEither { +export function fetchByGps(gps: LatLng): Promise { const { latitude, longitude } = gps; return fetchAndDecode( - `https://wind.waqi.info/mapq/nearest?geo=1/${latitude}/${longitude}`, - ByStationCodec + `https://wind.waqi.info/mapq/nearest?geo=1/${latitude}/${longitude}` ); } diff --git a/packages/dataproviders/src/providers/waqi/normalize.ts b/packages/dataproviders/src/providers/waqi/normalize.ts index b418b421..47a4b9cf 100644 --- a/packages/dataproviders/src/providers/waqi/normalize.ts +++ b/packages/dataproviders/src/providers/waqi/normalize.ts @@ -1,30 +1,24 @@ -import { convert, isPollutant, Pollutant, ugm3 } from '@shootismoke/convert'; -import * as E from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; +import { convert, isPollutant, ugm3 } from '@shootismoke/convert'; import { OpenAQResults } from '../../types'; import { getCountryCode, providerError } from '../../util'; import { sanitizeCountry } from '../aqicn/normalize'; -import { ByStation } from './validation'; +import { WaqiResponse } from './types'; /** - * Normalize aqicn byGps data + * Normalize aqicn byGps data. Throws an error if the data is invalid. * * @param data - The data to normalize */ -export function normalize({ - d: [data], -}: ByStation): E.Either { +export function normalize({ d: [data] }: WaqiResponse): OpenAQResults { const stationId = `waqi|${data.x}`; if (!isPollutant(data.pol)) { - return E.left( - providerError( - 'waqi', - `Cannot normalize station ${stationId}, unrecognized pollutant ${ - data.pol - }: ${JSON.stringify(data)}` - ) + throw providerError( + 'waqi', + `Cannot normalize station ${stationId}, unrecognized pollutant ${ + data.pol + }: ${JSON.stringify(data)}` ); } @@ -33,11 +27,9 @@ export function normalize({ const ugm3Value = convert('pm25', 'usaEpa', ugm3, aqiUS); if (!data.u.includes('/')) { - return E.left( - providerError( - 'waqi', - `Got invalid country/city info: ${JSON.stringify(data.u)}` - ) + throw providerError( + 'waqi', + `Got invalid country/city info: ${JSON.stringify(data.u)}` ); } const [country, city] = data.u.split('/'); @@ -46,36 +38,35 @@ export function normalize({ // Get UTC time const utc = new Date(+data.t * 1000).toISOString(); - return pipe( - getCountryCode(countryRaw), - E.fromOption(() => - providerError('waqi', `Cannot get code from country ${country}`) - ), - E.map((country) => [ - { - attribution: [{ name: data.nlo }], - averagingPeriod: { - unit: 'day', - value: 1, - }, - coordinates: { - latitude: data.geo[0], - longitude: data.geo[1], - }, - country, - city, - date: { - local: utc, // FIXME How should we get local time from UTC time? - utc, - }, - entity: 'other', - location: `waqi|${data.x}`, - isMobile: false, - parameter: data.pol as Pollutant, - sourceName: 'waqi', - unit: ugm3, // FIXME Once convert() supports ppm, we should use `getPollutantMeta(data.pol as Pollutant).preferredUnit` here - value: ugm3Value, + const countryCode = getCountryCode(countryRaw); + if (!countryCode) { + throw providerError('waqi', `Cannot get code from country ${country}`); + } + + return [ + { + attribution: [{ name: data.nlo }], + averagingPeriod: { + unit: 'day', + value: 1, + }, + coordinates: { + latitude: data.geo[0], + longitude: data.geo[1], + }, + country, + city, + date: { + local: utc, // FIXME How should we get local time from UTC time? + utc, }, - ]) - ); + entity: 'other', + location: `waqi|${data.x}`, + isMobile: false, + parameter: data.pol, + sourceName: 'waqi', + unit: ugm3, // FIXME Once convert() supports ppm, we should use `getPollutantMeta(data.pol as Pollutant).preferredUnit` here + value: ugm3Value, + }, + ]; } diff --git a/packages/dataproviders/src/providers/waqi/validation.ts b/packages/dataproviders/src/providers/waqi/types.ts similarity index 57% rename from packages/dataproviders/src/providers/waqi/validation.ts rename to packages/dataproviders/src/providers/waqi/types.ts index d8f376e2..f24815a9 100644 --- a/packages/dataproviders/src/providers/waqi/validation.ts +++ b/packages/dataproviders/src/providers/waqi/types.ts @@ -1,5 +1,3 @@ -import * as t from 'io-ts'; - // Example response: // Object { // "d": Array [ @@ -22,22 +20,18 @@ import * as t from 'io-ts'; // "g": null, // } -export const ByStationCodec = t.type({ - d: t.array( - t.type({ - d: t.number, - geo: t.tuple([t.number, t.number]), - key: t.string, - nlo: t.string, - nna: t.string, - pol: t.string, - t: t.number, - u: t.string, - v: t.string, - x: t.string, - }) - ), - g: t.any, -}); - -export type ByStation = t.TypeOf; +export type WaqiResponse = { + d: { + d: number; + geo: [number, number]; + key: string; + nlo: string; + nna: string; + pol: string; + t: number; + u: string; + v: string; + x: string; + }[]; + g: unknown; +}; diff --git a/packages/dataproviders/src/providers/waqi/waqi.spec.ts b/packages/dataproviders/src/providers/waqi/waqi.spec.ts index 8817ec14..621f0ac9 100644 --- a/packages/dataproviders/src/providers/waqi/waqi.spec.ts +++ b/packages/dataproviders/src/providers/waqi/waqi.spec.ts @@ -1,17 +1,9 @@ -import * as E from 'fp-ts/lib/Either'; - -import { ByStation } from './validation'; import { waqi } from './waqi'; describe('waqi', () => { describe('by station', () => { it('should throw on fetchByStation', async () => { - expect(await waqi.fetchByStation('foo')()).toEqual( - E.left(new Error('Unimplemented')) - ); - expect(waqi.normalizeByStation({} as ByStation)).toEqual( - E.left(new Error('Unimplemented')) - ); + await expect(waqi.fetchByStation('foo')).rejects.toThrow(); }); }); }); diff --git a/packages/dataproviders/src/providers/waqi/waqi.ts b/packages/dataproviders/src/providers/waqi/waqi.ts index aca2a839..e6532fed 100644 --- a/packages/dataproviders/src/providers/waqi/waqi.ts +++ b/packages/dataproviders/src/providers/waqi/waqi.ts @@ -1,19 +1,15 @@ -import * as E from 'fp-ts/lib/Either'; -import * as TE from 'fp-ts/lib/TaskEither'; - -import { ProviderFP } from '../types'; +import { Provider } from '../types'; import { fetchByGps } from './fetchBy'; import { normalize } from './normalize'; -import { ByStation } from './validation'; +import { WaqiResponse } from './types'; /** * @see https://wind.waqi.info/ */ -export const waqi: ProviderFP = { +export const waqi: Provider = { fetchByGps, - fetchByStation: () => TE.left(new Error('Unimplemented')), + fetchByStation: () => Promise.reject(new Error('Not implemented')), id: 'waqi', name: 'WAQI', - normalizeByGps: normalize, - normalizeByStation: () => E.left(new Error('Unimplemented')), + normalize, }; diff --git a/packages/dataproviders/src/util/codec.spec.ts b/packages/dataproviders/src/util/codec.spec.ts deleted file mode 100644 index b0b1912c..00000000 --- a/packages/dataproviders/src/util/codec.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as E from 'fp-ts/lib/Either'; - -import { ByStationCodec } from '../providers/aqicn/validation'; -import { decodeWith } from './codec'; - -describe('codec decode', () => { - it('should return left when decode fails', async () => { - expect(await decodeWith(ByStationCodec)('foo')()).toEqual( - E.left( - new Error( - 'Invalid value "foo" supplied to : { status: "ok" | "error" | "nope", data: ({ attributions: (Array<({ name: string } & Partial<{ url: string }>)> | null), city: { geo: ([(string | number), (string | number)] | null), name: (string | undefined), url: (string | undefined) }, dominentpol: (string | undefined), iaqi: ({ [K in string]: { v: number } } | undefined), idx: number, time: { s: (string | undefined), tz: (string | undefined), v: number, iso: (undefined | string) } } | string | undefined), msg: (string | undefined) }' - ) - ) - ); - }); -}); diff --git a/packages/dataproviders/src/util/codec.ts b/packages/dataproviders/src/util/codec.ts deleted file mode 100644 index 42624d6f..00000000 --- a/packages/dataproviders/src/util/codec.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @ignore - */ /** */ - -import axios from 'axios'; -import debug from 'debug'; -import * as E from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; -import * as TE from 'fp-ts/lib/TaskEither'; -import { Type } from 'io-ts'; -import { failure } from 'io-ts/lib/PathReporter'; - -import { promiseToTE } from './fp'; - -const l = debug('shootismoke:dataproviders'); - -/** - * Decode, and return an Error on failure - * - * @param codec - Codec used to decode - */ -export function decodeWith( - codec: Type -): (response: I) => TE.TaskEither { - return (response: I): TE.TaskEither => - TE.fromEither( - pipe( - codec.decode(response), - E.mapLeft((errors) => new Error(failure(errors).join('\n'))) - ) - ); -} - -/** - * Fetch with axios from URL, and decode using io-ts - * - * @param url - The URL to fetch from - * @param codec = The io-ts codec used for decoding - * @param options - Additional options, e.g. error handling - */ -export function fetchAndDecode( - url: string, - codec: Type, - options: { - onError?: (error: E) => Error; - } = {} -): TE.TaskEither { - l(`Fetching URL ${url}`); - - return pipe( - promiseToTE(() => - axios - .get(url) - .then(({ data }) => data as I) - .catch((error: E) => { - throw options.onError ? options.onError(error) : error; - }) - ), - TE.chain(decodeWith(codec)) - ); -} diff --git a/packages/dataproviders/src/util/constants.ts b/packages/dataproviders/src/util/constants.ts index b74daa77..550dbbef 100644 --- a/packages/dataproviders/src/util/constants.ts +++ b/packages/dataproviders/src/util/constants.ts @@ -3,8 +3,3 @@ * measurement to be okay. */ export const ACCURATE_RADIUS = 10000; - -/** - * List of all supported providers' code, in an array - */ -export const AllProviders = ['aqicn', 'openaq', 'waqi']; diff --git a/packages/dataproviders/src/util/countryCode.spec.ts b/packages/dataproviders/src/util/countryCode.spec.ts index 18d40aa3..c60b8e9e 100644 --- a/packages/dataproviders/src/util/countryCode.spec.ts +++ b/packages/dataproviders/src/util/countryCode.spec.ts @@ -1,27 +1,23 @@ -import * as O from 'fp-ts/lib/Option'; - import { getCountryCode } from './countryCode'; describe('getCountryCode', () => { it('should match exact country', () => { - expect(getCountryCode('United States')).toEqual(O.some('US')); + expect(getCountryCode('United States')).toEqual('US'); }); it('should match lower case', () => { - expect(getCountryCode('united states')).toEqual(O.some('US')); + expect(getCountryCode('united states')).toEqual('US'); }); it('should match multiple spacing', () => { - expect(getCountryCode('united states')).toEqual(O.some('US')); + expect(getCountryCode('united states')).toEqual('US'); }); it('should match dashed', () => { - expect(getCountryCode('saudi-arabia')).toEqual(O.some('SA')); + expect(getCountryCode('saudi-arabia')).toEqual('SA'); }); it('should match included string', () => { - expect(getCountryCode('United States Of America')).toEqual( - O.some('US') - ); + expect(getCountryCode('United States Of America')).toEqual('US'); }); }); diff --git a/packages/dataproviders/src/util/countryCode.ts b/packages/dataproviders/src/util/countryCode.ts index cf1a54b3..4579187d 100644 --- a/packages/dataproviders/src/util/countryCode.ts +++ b/packages/dataproviders/src/util/countryCode.ts @@ -1,6 +1,3 @@ -import * as O from 'fp-ts/lib/Option'; -import { pipe } from 'fp-ts/lib/pipeable'; - import countries from './countries.json'; interface Country { @@ -35,23 +32,18 @@ function sanitize(input: string): string { * getCountryCode('united States'); // 'US' * ``` */ -export function getCountryCode(input: string): O.Option { - return pipe( - O.fromNullable( - (countries as Country[]).find(({ code, name, others }) => { - const sName = sanitize(name); - const sInput = sanitize(input); +export function getCountryCode(input: string): string | undefined { + return (countries as Country[]).find(({ code, name, others }) => { + const sName = sanitize(name); + const sInput = sanitize(input); - return ( - sName === sInput || - code === sInput || - sInput.includes(sName) || - others.includes(sInput) - ); - }) - ), - O.map(({ code }) => code) - ); + return ( + sName === sInput || + code === sInput || + sInput.includes(sName) || + others.includes(sInput) + ); + })?.code; } /** diff --git a/packages/dataproviders/src/util/fetch.ts b/packages/dataproviders/src/util/fetch.ts new file mode 100644 index 00000000..d9726e81 --- /dev/null +++ b/packages/dataproviders/src/util/fetch.ts @@ -0,0 +1,19 @@ +import axios from 'axios'; +import debug from 'debug'; + +const l = debug('shootismoke:dataproviders'); + +export function fetchAndDecode( + url: string, + options: { + formatError?: (error: E) => Error; + } = {} +): Promise { + l(`Fetching URL ${url}`); + return axios + .get(url) + .then(({ data }) => data) + .catch((error: E) => { + throw options.formatError ? options.formatError(error) : error; + }); +} diff --git a/packages/dataproviders/src/util/fp.spec.ts b/packages/dataproviders/src/util/fp.spec.ts deleted file mode 100644 index 64d91b46..00000000 --- a/packages/dataproviders/src/util/fp.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as E from 'fp-ts/lib/Either'; - -import { promiseToTE } from './fp'; - -describe('promiseToTE', () => { - it('should correctly convert to a TaskEither Right', async () => { - const promise = (): Promise => Promise.resolve(42); - const e = await promiseToTE(promise)(); - - expect(e).toEqual(E.right(42)); - }); - - it('should correctly convert to a TaskEither Left (string)', async () => { - const promise = (): Promise => Promise.reject('foo'); - const e = await promiseToTE(promise)(); - - expect(e).toEqual(E.left(new Error('foo'))); - }); - - it('should correctly convert to a TaskEither Left (Error)', async () => { - const promise = (): Promise => Promise.reject(new Error('foo')); - const e = await promiseToTE(promise)(); - - expect(e).toEqual(E.left(new Error('foo'))); - }); -}); diff --git a/packages/dataproviders/src/util/fp.ts b/packages/dataproviders/src/util/fp.ts deleted file mode 100644 index 5a301b8e..00000000 --- a/packages/dataproviders/src/util/fp.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @ignore - */ /** */ - -import * as E from 'fp-ts/lib/Either'; -import { Lazy } from 'fp-ts/lib/function'; -import { pipe } from 'fp-ts/lib/pipeable'; -import * as T from 'fp-ts/lib/Task'; -import * as TE from 'fp-ts/lib/TaskEither'; - -/** - * Convert a Promise into a TaskEither - * - * @param fn - Function returning a Promise - */ -export function promiseToTE(fn: Lazy>): TE.TaskEither { - return TE.tryCatch(fn, (reason: Error | unknown) => { - const error = - reason instanceof Error ? reason : new Error(String(reason)); - - return error; - }); -} - -/** - * Convert a TaskEither into a Promise - * - * @param fn - Function returning a Promise - */ -export function teToPromise(te: TE.TaskEither): Promise { - return new Promise((resolve, reject) => { - pipe( - te, - TE.fold( - (error) => { - reject(error); - - return T.of(undefined); - }, - (data) => { - resolve(data); - - return T.of(undefined); - } - ) - )().catch(reject); - }); -} - -/** - * Convert an Either into a A, or throw if error - */ -export function eitherToFunction(e: E.Either): A { - return pipe( - e, - E.fold( - (error) => { - throw error; - }, - (data) => data - ) - ); -} diff --git a/packages/dataproviders/src/util/index.ts b/packages/dataproviders/src/util/index.ts index ecc9d0d6..600c3690 100644 --- a/packages/dataproviders/src/util/index.ts +++ b/packages/dataproviders/src/util/index.ts @@ -1,8 +1,6 @@ -export * from './codec'; export * from './constants'; export * from './countryCode'; export * from './errors'; -export * from './fp'; export * from './getDominantPol'; export * from './openaq'; export * from './stationName'; diff --git a/packages/dataproviders/src/util/openaq.ts b/packages/dataproviders/src/util/openaq.ts index b03f7634..99459179 100644 --- a/packages/dataproviders/src/util/openaq.ts +++ b/packages/dataproviders/src/util/openaq.ts @@ -1,114 +1,16 @@ -import { AllPollutants, AllUnits, Pollutant, Unit } from '@shootismoke/convert'; -import * as t from 'io-ts'; +import { Pollutant, Unit } from '@shootismoke/convert'; -/** - * @ignore - */ -export const attributionsCodec = t.union([ - t.array( - t.intersection([ - t.type({ - name: t.string, - }), - t.partial({ - url: t.string, - }), - ]) - ), - t.null, -]); - -/** - * @ignore - */ -export const latLngCodec = t.union([ - t.type({ - latitude: t.number, - longitude: t.number, - }), - t.null, -]); - -/** - * @ignore - */ -export const pollutantCodec = t.union( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore We're sure there's at list one element. - AllPollutants.map((pol) => t.literal(pol)) -); - -/** - * @ignores - */ -export const unitCodec = t.union( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore We're sure there's at list one element. - AllUnits.map((u) => t.literal(u)) -); - -/** - * @ignore - */ -export const entityCodec = t.union([ - t.literal('community'), - t.literal('government'), - t.literal('research'), - t.literal('other'), -]); - -/** - * Required fields for OpenAQ data format. - * - * This is empirical! It is gathered from looking at multiple endpoints. - * @see https://github.com/openaq/openaq-data-format - */ -const OpenAQCodecRequired = t.type({ - country: t.string, - date: t.type({ - local: t.string, - utc: t.string, - }), - location: t.string, - parameter: pollutantCodec, - value: t.number, - unit: unitCodec, -}); +type attributions = { + name: string; + url?: string | undefined; +}[]; -/** - * Optional fields for OpenAQ data format - * - * This is empirical! It is gathered from looking at multiple endpoints. - * @see https://github.com/openaq/openaq-data-format - * @ignore - */ -export const OpenAQCodecOptional = t.partial({ - attribution: attributionsCodec, - averagingPeriod: t.type({ - unit: t.string, - value: t.union([t.number, t.null]), - }), - city: t.union([t.string, t.null]), - coordinates: latLngCodec, - entity: entityCodec, - isAnalysis: t.boolean, - isMobile: t.boolean, - sourceName: t.string, - sensorType: t.string, -}); - -/** - * An io-ts codec to validate the v2 OpenAQ data format. - * - * This is empirical! It is gathered from looking at multiple endpoints. - * @see https://github.com/openaq/openaq-data-format - */ -export const OpenAQCodec = t.intersection([ - OpenAQCodecRequired, - OpenAQCodecOptional, -]); +type latLng = { + latitude: number; + longitude: number; +}; -type OpenAQResultBase = t.TypeOf; +type entity = 'community' | 'government' | 'research' | 'other'; /** * A TypeScript type to represent the OpenAQ data format. @@ -118,38 +20,48 @@ type OpenAQResultBase = t.TypeOf; * * */ -export interface OpenAQResult extends OpenAQResultBase { - parameter: Pollutant; // These two fields are not statically inferred by TS. So we hardcode them. +export interface OpenAQResult { + parameter: Pollutant; unit: Unit; + country: string; + date: { + local: string; + utc: string; + }; + location: string; + value: number; + attribution?: attributions; + averagingPeriod?: { unit: string; value?: number }; + city?: string; + coordinates?: latLng; + entity?: entity; + isAnalysis?: boolean; + isMobile?: boolean; + sourceName?: string; + sensorType?: string; } -/** - * OpenAQ Error format. - * - * @see https://docs.openaq.org/# - * @ignore - */ -const OpenAQErrorObjectCodec = t.type({ - detail: t.array( - t.type({ - loc: t.array(t.string), - msg: t.string, - type: t.string, - }) - ), -}); - -/** - * An io-ts codec to validate the v2 OpenAQ error format. - */ -export const OpenAQErrorCodec = t.union([OpenAQErrorObjectCodec, t.string]); // We sometimes also get string. - /** * @ignore */ -export type OpenAQErrorObject = t.TypeOf; +export type OpenAQErrorObject = { + detail: { + loc: string[]; + msg: string; + type: string; + }[]; +}; /** * Type of the v2 OpenAQ error format. + * We sometimes (rarely) get a string. */ -export type OpenAQError = t.TypeOf; +export type OpenAQError = + | string + | { + detail: { + loc: string[]; + msg: string; + type: string; + }[]; + }; diff --git a/packages/dataproviders/test/e2e/aqicn.spec.ts b/packages/dataproviders/test/e2e/aqicn.spec.ts index 479d9e91..abf77f73 100644 --- a/packages/dataproviders/test/e2e/aqicn.spec.ts +++ b/packages/dataproviders/test/e2e/aqicn.spec.ts @@ -1,7 +1,5 @@ -import * as E from 'fp-ts/lib/Either'; - import { aqicn } from '../../src/providers/aqicn'; -import { testProviderE2E, testTE } from '../util/testUtil'; +import { testPromise, testProviderE2E } from '../util/testUtil'; const options = { token: process.env.AQICN_TOKEN as string, @@ -11,37 +9,37 @@ describe('aqicn e2e', () => { beforeAll(() => jest.setTimeout(30000)); it('should return an error with an unknown station', async () => { - expect(await aqicn.fetchByStation('foo', options)()).toEqual( - E.left(new Error('Unknown station')) + await expect(aqicn.fetchByStation('foo', options)).rejects.toThrowError( + new Error('Unknown station') ); }); describe('fetchByGps sanitize.json mapping', () => { it('should fetch paris', () => - testTE( + testPromise( aqicn.fetchByGps( { latitude: 48.8546, longitude: 2.34771 }, options ), - (d) => aqicn.normalizeByGps(d) + (d) => aqicn.normalize(d) )); it('should fetch pune', () => - testTE( + testPromise( aqicn.fetchByGps( { latitude: 18.5203, longitude: 73.8543 }, options ), - (d) => aqicn.normalizeByGps(d) + (d) => aqicn.normalize(d) )); it('should fetch beijing', () => - testTE( + testPromise( aqicn.fetchByGps( { latitude: 39.9289, longitude: 116.3883 }, options ), - (d) => aqicn.normalizeByGps(d) + (d) => aqicn.normalize(d) )); }); diff --git a/packages/dataproviders/test/e2e/openaq.spec.ts b/packages/dataproviders/test/e2e/openaq.spec.ts index c31847bc..9346cb69 100644 --- a/packages/dataproviders/test/e2e/openaq.spec.ts +++ b/packages/dataproviders/test/e2e/openaq.spec.ts @@ -1,7 +1,4 @@ import { subDays } from 'date-fns'; -import { pipe } from 'fp-ts/lib/pipeable'; -import * as T from 'fp-ts/lib/Task'; -import * as TE from 'fp-ts/lib/TaskEither'; import { openaq } from '../../src/providers/openaq'; import { testProviderE2E } from '../util/testUtil'; @@ -13,63 +10,41 @@ describe('openaq e2e', () => { skip: ['fetchByStation'], }); - it('should fetch station Beijing', () => { - return pipe( - openaq.fetchByStation('Beijing'), - TE.fold( - (error) => { - throw error; - }, - ({ results }) => { - expect(results.length).toBeGreaterThanOrEqual(1); - - return T.of(void undefined); - } - ) - )(); + it('should fetch station Beijing', async () => { + const { results } = await openaq.fetchByStation('Beijing'); + expect(results.length).toBeGreaterThanOrEqual(1); }); - it('should fetch correctly with options', () => { + it('should fetch correctly with options', async () => { const dateFrom = subDays(new Date(), 9000); const dateTo = subDays(new Date(), 7); - return pipe( - openaq.fetchByStation('Beijing', { - dateFrom, - dateTo, - limit: 2, - includeFields: ['sourceName', 'isMobile', 'entity'], - parameter: ['pm25'], - }), - TE.fold( - (error) => { - throw error; - }, - ({ results }) => { - // Check limit. - expect(results.length).toBeLessThanOrEqual(3); // Somehow when we fetch limit=2, it returns 3 results... - // Check parameter. - expect( - results.some(({ parameter }) => parameter !== 'pm25') - ).toBe(false); - - results.forEach((result) => { - // Check dateFrom & dateTo. - const measurementDate = new Date(result.date.utc); - expect( - dateFrom <= measurementDate && - measurementDate <= dateTo - ).toBe(true); - // Check includeFields. - expect(result.isMobile).not.toBeUndefined(); - expect(result.entity).toBeTruthy(); - expect(result.sensorType).toBeTruthy(); - }); - - return T.of(void undefined); - } - ) - )(); + const { results } = await openaq.fetchByStation('Beijing', { + dateFrom, + dateTo, + limit: 2, + includeFields: ['sourceName', 'isMobile', 'entity'], + parameter: ['pm25'], + }); + + // Check limit. + expect(results.length).toBeLessThanOrEqual(3); // Somehow when we fetch limit=2, it returns 3 results... + // Check parameter. + expect(results.some(({ parameter }) => parameter !== 'pm25')).toBe( + false + ); + + results.forEach((result) => { + // Check dateFrom & dateTo. + const measurementDate = new Date(result.date.utc); + expect( + dateFrom <= measurementDate && measurementDate <= dateTo + ).toBe(true); + // Check includeFields. + expect(result.isMobile).not.toBeUndefined(); + expect(result.entity).toBeTruthy(); + expect(result.sensorType).toBeTruthy(); + }); }); afterAll(() => jest.setTimeout(5000)); diff --git a/packages/dataproviders/test/e2e/promise.spec.ts b/packages/dataproviders/test/e2e/promise.spec.ts index 5d6ed7f8..829fe17b 100644 --- a/packages/dataproviders/test/e2e/promise.spec.ts +++ b/packages/dataproviders/test/e2e/promise.spec.ts @@ -1,4 +1,4 @@ -import { aqicn, openaq } from '../../src/promise'; +import { aqicn, openaq } from '../../src'; describe('promise', () => { beforeAll(() => jest.setTimeout(30000)); @@ -7,7 +7,7 @@ describe('promise', () => { const data = await openaq.fetchByStation('Coyhaique'); expect(data.results.length).toBeGreaterThanOrEqual(1); - const results = openaq.normalizeByStation(data); + const results = openaq.normalize(data); expect(results[0].value).toBeDefined(); }); diff --git a/packages/dataproviders/test/util/testUtil.ts b/packages/dataproviders/test/util/testUtil.ts index 5011d6a9..18631e15 100644 --- a/packages/dataproviders/test/util/testUtil.ts +++ b/packages/dataproviders/test/util/testUtil.ts @@ -1,10 +1,6 @@ import { AllPollutants } from '@shootismoke/convert'; -import * as E from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; -import * as T from 'fp-ts/lib/Task'; -import * as TE from 'fp-ts/lib/TaskEither'; -import type { ProviderFP } from '../../src/providers/types'; +import type { Provider } from '../../src/providers/types'; import type { LatLng, OpenAQResults } from '../../src/types'; function generateRandomLatLng(): LatLng { @@ -36,51 +32,38 @@ export function testOpenAQResults(results: OpenAQResults): void { } /** - * Test that a TE is resolving correctly. + * Test that a Promise is resolving correctly. */ -export function testTE( - te: TE.TaskEither, - normalize: (data: T) => E.Either +export async function testPromise( + p: Promise, + normalize: (data: T) => OpenAQResults ): Promise { - return pipe( - te, - TE.map((response) => { - expect(response).toBeDefined(); + const res = await p; + expect(res).toBeDefined(); - return response; - }), - TE.chain((response) => TE.fromEither(normalize(response))), - TE.fold( - (error) => { - // We don't fail the test if one of the following errors occur - const skippedErrorMessages = [ - // Skip if the random stationId is an unknown station - 'Unknown ID', - // Skip if we somehow couldn't connect - 'can not connect', - // Skip if openaq doesn't return results - '[openaq] Cannot normalize, got 0 result', - // Skip if aqicn doesn't track pollutants that don't interest us - 'no pollutants currently tracked', - // Skip if aqicn doesn't expose city - 'no city', - ]; + try { + normalize(res); + } catch (error) { + // We don't fail the test if one of the following errors occur + const skippedErrorMessages = [ + // Skip if the random stationId is an unknown station + 'Unknown ID', + // Skip if we somehow couldn't connect + 'can not connect', + // Skip if openaq doesn't return results + '[openaq] Cannot normalize, got 0 result', + // Skip if aqicn doesn't track pollutants that don't interest us + 'no pollutants currently tracked', + // Skip if aqicn doesn't expose city + 'no city', + ]; - if ( - skippedErrorMessages.some((msg) => - error.message.includes(msg) - ) - ) { - return T.of(void undefined); - } - - throw error; - }, - () => { - return T.of(void undefined); - } - ) - )(); + expect( + skippedErrorMessages.some((msg) => + (error as Error).message.includes(msg) + ) + ).toBe(true); + } } interface TestProviderE2EOptions { @@ -89,10 +72,10 @@ interface TestProviderE2EOptions { } /** - * Test helper to test a providerFP + * Test helper to test a provider */ -export function testProviderE2E( - providerFP: ProviderFP, +export function testProviderE2E( + provider: Provider, { options, skip = [] }: TestProviderE2EOptions ): void { jest.setTimeout(30000); @@ -100,15 +83,15 @@ export function testProviderE2E( if (!skip.includes('fetchByGps')) { describe('fetchByGps', () => { [0, 0].map(generateRandomLatLng).forEach((gps) => { - it(`should fetch ${providerFP.id} by gps: ${JSON.stringify( + it(`should fetch ${provider.id} by gps: ${JSON.stringify( gps )}`, () => - testTE( - providerFP.fetchByGps( + testPromise( + provider.fetchByGps( { latitude: 78.7, longitude: 55.69 }, options ), - (d) => providerFP.normalizeByGps(d) + (d) => provider.normalize(d) )); }); }); @@ -117,9 +100,10 @@ export function testProviderE2E( if (!skip.includes('fetchByStation')) { describe('fetchByStation', () => { [0, 0].map(generateRandomStationId).forEach((stationId) => { - it(`should fetch ${providerFP.id} by station: ${stationId}`, () => - testTE(providerFP.fetchByStation(stationId, options), (d) => - providerFP.normalizeByStation(d) + it(`should fetch ${provider.id} by station: ${stationId}`, () => + testPromise( + provider.fetchByStation(stationId, options), + (d) => provider.normalize(d) )); }); }); diff --git a/packages/ui/package.json b/packages/ui/package.json index a88f6162..46928a1c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -28,9 +28,7 @@ "haversine": "^1.1.1", "lottie-react-native": "^5.1.4", "mongoose": "^6.6.3", - "p-any": "^3.0.0", - "react-native-web": "^0.18.9", - "retry-ts": "^0.1.4" + "p-any": "^3.0.0" }, "devDependencies": { "@types/react": "^18.0.21", @@ -39,7 +37,6 @@ "react-native": "https://github.com/expo/react-native/archive/sdk-40.0.0.tar.gz" }, "peerDependencies": { - "react": "^18.2.0", - "react-native": "https://github.com/expo/react-native/archive/sdk-40.0.0.tar.gz" + "react": "^18.2.0" } } diff --git a/packages/ui/src/components/BoxButton/BoxButton.tsx b/packages/ui/src/components/BoxButton/BoxButton.tsx deleted file mode 100644 index 528f59ed..00000000 --- a/packages/ui/src/components/BoxButton/BoxButton.tsx +++ /dev/null @@ -1,76 +0,0 @@ -// Sh**t! I Smoke -// Copyright (C) 2018-2021 Marcelo S. Coelho, Amaury M. - -// Sh**t! I Smoke is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Sh**t! I Smoke is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Sh**t! I Smoke. If not, see . - -import React from 'react'; -import { - StyleSheet, - Text, - TouchableWithoutFeedback, - TouchableWithoutFeedbackProps, - View, -} from 'react-native'; - -import * as theme from '../../util/theme'; - -interface BoxButtonProps extends TouchableWithoutFeedbackProps { - active?: boolean; - children: string | JSX.Element; -} - -const styles = StyleSheet.create({ - activeText: { - opacity: 1, - }, - boxButton: { - ...theme.elevationShadowStyle(3), - backgroundColor: 'white', - borderColor: 'rgba(0, 0, 0, 0.1)', - borderRadius: 12, - borderWidth: 1, - marginBottom: theme.spacing.mini, - paddingHorizontal: theme.spacing.small, - paddingVertical: 6, // Padding for the shadow - shadowOpacity: 0.1, - }, - boxButtonText: { - ...theme.shitText, - opacity: theme.disabledOpacity, - textAlign: 'center', - }, -}); - -export function BoxButton(props: BoxButtonProps): React.ReactElement { - const { active, children, onPress, style, ...rest } = props; - - return ( - - - {typeof children === 'string' ? ( - - {children} - - ) : ( - children - )} - - - ); -} diff --git a/packages/ui/src/components/BoxButton/index.ts b/packages/ui/src/components/BoxButton/index.ts deleted file mode 100644 index 7d963734..00000000 --- a/packages/ui/src/components/BoxButton/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Sh**t! I Smoke -// Copyright (C) 2018-2021 Marcelo S. Coelho, Amaury M. - -// Sh**t! I Smoke is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Sh**t! I Smoke is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Sh**t! I Smoke. If not, see . - -export * from './BoxButton'; diff --git a/packages/ui/src/components/Cigarettes/Cigarette/Cigarette.tsx b/packages/ui/src/components/Cigarettes/Cigarette/Cigarette.tsx deleted file mode 100644 index 4edba595..00000000 --- a/packages/ui/src/components/Cigarettes/Cigarette/Cigarette.tsx +++ /dev/null @@ -1,225 +0,0 @@ -// Sh**t! I Smoke -// Copyright (C) 2018-2021 Marcelo S. Coelho, Amaury M. - -// Sh**t! I Smoke is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Sh**t! I Smoke is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Sh**t! I Smoke. If not, see . - -import React from 'react'; -import { - Image, - ImageSourcePropType, - StyleProp, - StyleSheet, - View, - ViewStyle, -} from 'react-native'; - -import butt from '../../../../assets/images/butt.png'; -import buttVertical from '../../../../assets/images/butt-vertical.png'; -import head from '../../../../assets/images/head.png'; -import headVertical from '../../../../assets/images/head-vertical.png'; - -export type CigaretteOrientation = 'diagonal' | 'horizontal' | 'vertical'; - -interface CigaretteProps { - percentage: number; - orientation: CigaretteOrientation; - fullCigaretteLength: number; - style?: StyleProp; -} - -const styles = StyleSheet.create({ - butt: { - bottom: 0, - position: 'absolute', - left: 0, - }, - cigarette: { - flexGrow: 1, - }, - diagonal: { - transform: [{ rotate: '45deg' }, { scale: 1 }], - }, - head: { - position: 'absolute', - right: 0, - top: 0, - zIndex: 1, - }, - inner: { - bottom: 0, - left: 0, - overflow: 'hidden', - position: 'absolute', - }, -}); - -/** - * The percentage of cigarette length when `percentage=0`. - */ -const MIN_PERCENTAGE = 0.4; - -/** - * Given the full length of a cigarette, and the percentage of the cigarette - * smoked, get the actual length of the cigarette. - */ -function getCigaretteActualLength( - fullCigaretteLength: number, - percentage: number -): number { - return Math.ceil( - ((1 - MIN_PERCENTAGE) * percentage + MIN_PERCENTAGE) * - fullCigaretteLength - ); -} - -/** - * A cigarette's width:height aspect ratio. - */ -const CIGARETTE_ASPECT_RATIO = 21 / 280; -const CIGARETTE_HEAD_HW_RATIO = 27 / 20; - -function getContainerStyle( - orientation: CigaretteOrientation, - fullCigaretteLength: number -): ViewStyle { - // Assuming the cigarette is vertical: - const height = fullCigaretteLength; - const width = height * CIGARETTE_ASPECT_RATIO; - - switch (orientation) { - case 'horizontal': { - return { - height: width, - width: height, - }; - } - case 'vertical': { - return { - height: height, - width: width, - }; - } - default: - return {}; - } -} - -/** - * Render a horizontal or vertical cigarette. - */ -function renderCigarette( - orientation: 'horizontal' | 'vertical', - percentage: number, - fullCigaretteLength: number, - additionalStyle?: StyleProp -): React.ReactElement { - // Assuming cigarette is vertical: - const height = fullCigaretteLength; - const width = height * CIGARETTE_ASPECT_RATIO; - const actualHeight = getCigaretteActualLength( - fullCigaretteLength, - percentage - ); - - return ( - - - - - - - ); -} - -export function Cigarette(props: CigaretteProps): React.ReactElement { - const { orientation, percentage, fullCigaretteLength, style } = props; - - // Only used for diagonal. Assuming cigarette is vertical: - const height = getCigaretteActualLength(fullCigaretteLength, percentage); - const width = fullCigaretteLength * CIGARETTE_ASPECT_RATIO; - - // For diagonal cigarettes, we render a horizontal cigarette, and rotate it - // 45deg. - return orientation === 'diagonal' ? ( - - - {renderCigarette('horizontal', percentage, fullCigaretteLength)} - - - ) : ( - renderCigarette(orientation, percentage, fullCigaretteLength, style) - ); -} diff --git a/packages/ui/src/components/Cigarettes/Cigarette/index.ts b/packages/ui/src/components/Cigarettes/Cigarette/index.ts deleted file mode 100644 index 131ae635..00000000 --- a/packages/ui/src/components/Cigarettes/Cigarette/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Sh**t! I Smoke -// Copyright (C) 2018-2021 Marcelo S. Coelho, Amaury M. - -// Sh**t! I Smoke is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Sh**t! I Smoke is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Sh**t! I Smoke. If not, see . - -export * from './Cigarette'; diff --git a/packages/ui/src/components/Cigarettes/Cigarettes.tsx b/packages/ui/src/components/Cigarettes/Cigarettes.tsx deleted file mode 100644 index 84ee7a83..00000000 --- a/packages/ui/src/components/Cigarettes/Cigarettes.tsx +++ /dev/null @@ -1,140 +0,0 @@ -// Sh**t! I Smoke -// Copyright (C) 2018-2021 Marcelo S. Coelho, Amaury M. - -// Sh**t! I Smoke is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Sh**t! I Smoke is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Sh**t! I Smoke. If not, see . - -import React from 'react'; -import { StyleProp, StyleSheet, View, ViewProps } from 'react-native'; - -import { round } from '../../util/api'; -import { Cigarette } from './Cigarette'; - -export interface CigarettesProps extends ViewProps { - /** - * The number of cigarettes to show. - */ - cigarettes: number; - /** - * Additional styling for inner cigarettes - */ - cigaretteStyle?: StyleProp; - /** - * Length, in pixels, of a full cigarette. - * - * @default 90 - */ - fullCigaretteLength?: number; - /** - * The maximum number of cigarettes to show. - * - * @default 50 - */ - showMaxCigarettes?: number; - /** - * For small amount of cigarettes, we display them horizontally. After this - * number, they are displayed vertically. - * - * @default 4 - */ - showVerticalAfter?: number; - /** - * Horizontal spacing, in pixels, between the cigarettes, assuming the - * cigarettes are displayed vertically. - * - * @default 5 - */ - spacingHorizontal?: number; - /** - * Vertical spacing, in pixels, between the cigarettes, assuming the - * cigarettes are displayed vertically. - * - * @default 20 - */ - spacingVertical?: number; -} - -const styles = StyleSheet.create({ - innerContainer: { - alignContent: 'flex-end', - alignItems: 'flex-end', - flexDirection: 'row', - flexWrap: 'wrap', - }, -}); - -export function Cigarettes(props: CigarettesProps): React.ReactElement { - const { - cigarettes: realCigarettes, - cigaretteStyle, - fullCigaretteLength = 90, - showMaxCigarettes = 50, - showVerticalAfter = 4, - spacingHorizontal = 5, - spacingVertical = 20, - style, - ...rest - } = props; - - // We don't show more than `showMaxCigarettes` cigarettes, and we round to - // 0.1. - const cigarettes = round( - Math.max(0.1, Math.min(realCigarettes, showMaxCigarettes)) - ); - - const count = Math.floor(cigarettes); // The cigarette count, without decimal. - const decimal = cigarettes - count; - - const orientation = - cigarettes <= 1 - ? 'diagonal' - : cigarettes <= showVerticalAfter - ? 'horizontal' - : 'vertical'; - const baseCigaretteStyle = - orientation === 'diagonal' - ? undefined - : orientation === 'horizontal' - ? { - marginBottom: spacingHorizontal, - marginRight: spacingVertical, - } - : { - marginBottom: spacingVertical, - marginRight: spacingHorizontal, - }; - - return ( - - {cigarettes > 1 && - count >= 1 && - Array.from(Array(count)).map((_, i) => ( - - ))} - {(cigarettes === 1 || decimal > 0) && ( - - )} - - ); -} diff --git a/packages/ui/src/components/Cigarettes/index.ts b/packages/ui/src/components/Cigarettes/index.ts deleted file mode 100644 index 3a2c0f03..00000000 --- a/packages/ui/src/components/Cigarettes/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Sh**t! I Smoke -// Copyright (C) 2018-2021 Marcelo S. Coelho, Amaury M. - -// Sh**t! I Smoke is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Sh**t! I Smoke is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Sh**t! I Smoke. If not, see . - -export * from './Cigarettes'; diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts deleted file mode 100644 index 62daa5ad..00000000 --- a/packages/ui/src/components/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Sh**t! I Smoke -// Copyright (C) 2018-2021 Marcelo S. Coelho, Amaury M. - -// Sh**t! I Smoke is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Sh**t! I Smoke is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Sh**t! I Smoke. If not, see . - -export * from './BoxButton'; -export * from './Cigarettes'; diff --git a/packages/ui/src/util/api.ts b/packages/ui/src/util/api.ts index 275cb682..0a6c1376 100644 --- a/packages/ui/src/util/api.ts +++ b/packages/ui/src/util/api.ts @@ -129,13 +129,13 @@ export function createApi( /** * Helper function to fetch & normalize data for 1 provider. */ -async function fetchForProvider( +async function fetchForProvider( gps: LatLng, - provider: Provider, + provider: Provider, options?: Options ): Promise { const data = await provider.fetchByGps(gps, options); - const results = provider.normalizeByGps(data); + const results = provider.normalize(data); l(`Got data from ${provider.id}: ${JSON.stringify(results)}`); return results; diff --git a/packages/ui/src/util/fp.ts b/packages/ui/src/util/fp.ts deleted file mode 100644 index 80751dc4..00000000 --- a/packages/ui/src/util/fp.ts +++ /dev/null @@ -1,101 +0,0 @@ -// Sh**t! I Smoke -// Copyright (C) 2018-2021 Marcelo S. Coelho, Amaury M. - -// Sh**t! I Smoke is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Sh**t! I Smoke is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Sh**t! I Smoke. If not, see . - -import * as C from 'fp-ts/lib/Console'; -import * as E from 'fp-ts/lib/Either'; -import * as O from 'fp-ts/lib/Option'; -import { pipe } from 'fp-ts/lib/pipeable'; -import * as T from 'fp-ts/lib/Task'; -import * as TE from 'fp-ts/lib/TaskEither'; -import { - capDelay, - exponentialBackoff, - limitRetries, - monoidRetryPolicy, - RetryStatus, -} from 'retry-ts'; -import { retrying } from 'retry-ts/lib/Task'; -export { promiseToTE } from '@shootismoke/dataproviders'; - -/** - * A side-effect in a TaskEither chain: if the TaskEither fails, still return - * a TaskEither.Right - * - * @example - * ``` - * function myTe(value: number) { // A TaskEither }; - * - * pipe( - * TE.of(1), - * TE.chain(sideEffect(myTe) - * ) - * ``` - */ -export function sideEffect(fn: (input: A) => TE.TaskEither) { - return (input: A): TE.TaskEither => - TE.rightTask( - pipe( - fn(input), - TE.fold( - (error) => - pipe( - T.fromIO(C.log(error)), - T.map(() => input) - ), - () => T.of(input) - ) - ) - ); -} - -export interface RetryOptions { - capDelay?: number; - exponentialBackoff?: number; - retries?: number; -} - -/** - * Retry a TaskEither. - * - * @param retries - The number of time to retry - * @param teFn - A function returning a TE - */ -export function retry( - teFn: (status: RetryStatus, delay: number) => TE.TaskEither, - options: RetryOptions = {} -): TE.TaskEither { - // Set our retry policy - const policy = capDelay( - options.capDelay || 2000, - monoidRetryPolicy.concat( - exponentialBackoff(options.exponentialBackoff || 200), - limitRetries(options.retries || 3) - ) - ); - - return retrying( - policy, - (status) => - pipe( - status.previousDelay, - O.fold( - () => TE.left(new Error('Empty Option')), - (delay) => teFn(status, delay) - ) - ), - E.isLeft - ); -} diff --git a/packages/ui/src/util/geoapify.ts b/packages/ui/src/util/geoapify.ts index 342addf7..81ce4c35 100644 --- a/packages/ui/src/util/geoapify.ts +++ b/packages/ui/src/util/geoapify.ts @@ -15,15 +15,17 @@ // along with Sh**t! I Smoke. If not, see . import { LatLng } from '@shootismoke/dataproviders'; +import retry from 'async-retry'; import axios from 'axios'; -import * as E from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; -import * as T from 'fp-ts/lib/Task'; -import * as TE from 'fp-ts/lib/TaskEither'; -import * as t from 'io-ts'; -import { failure } from 'io-ts/lib/PathReporter'; -import { promiseToTE, retry } from './fp'; +export interface GeoapifyRes { + city?: string; + country?: string; + formatted: string; + lat: number; + lon: number; + state?: string; +} /** * For docs, see https://apidocs.geoapify.com/playground/geocoding and @@ -41,58 +43,24 @@ function getEndpoint(search: string, apiKey: string, gps?: LatLng): string { return base; } -const GeoapifyResT = t.exact( - t.intersection([ - t.type({ - lat: t.number, - lon: t.number, - formatted: t.string, - }), - t.partial({ - city: t.string, - country: t.string, - state: t.string, - }), - ]) -); - -export type GeoapifyRes = t.TypeOf; - -const AxiosResponseT = t.partial({ - data: t.type({ - results: t.array(GeoapifyResT), - }), -}); - -export function geoapify( +export async function geoapify( search: string, apiKey: string, gps?: LatLng -): TE.TaskEither { +): Promise { return retry( - () => - pipe( - promiseToTE(() => - axios.get(getEndpoint(search, apiKey, gps), { - timeout: 10000, - }) - ), - TE.chain((response) => - T.of( - pipe( - AxiosResponseT.decode(response), - E.mapLeft(failure), - E.mapLeft((errs) => errs[0]), // Only show 1st error - E.mapLeft(Error) - ) - ) - ), - TE.chain((response) => - response.data?.results - ? TE.right(response.data.results) - : TE.left(new Error('No data returned by geoapify')) - ) - ), + async () => { + const { + data: { results }, + } = await axios.get<{ results: GeoapifyRes[] }>( + getEndpoint(search, apiKey, gps), + { + timeout: 5000, + } + ); + + return results; + }, { retries: 2, } diff --git a/packages/ui/src/util/index.ts b/packages/ui/src/util/index.ts index 5a720f43..b36b8224 100644 --- a/packages/ui/src/util/index.ts +++ b/packages/ui/src/util/index.ts @@ -17,7 +17,6 @@ export * from './api'; export * from './geoapify'; export * from './frequency'; -export * from './fp'; export * from './noop'; export * from './pollutant'; export * from './provider'; diff --git a/packages/ui/src/util/provider.ts b/packages/ui/src/util/provider.ts index cc29895e..a9d3fc67 100644 --- a/packages/ui/src/util/provider.ts +++ b/packages/ui/src/util/provider.ts @@ -1,9 +1,10 @@ -import { AllProviders, aqicn, openaq, waqi } from '@shootismoke/dataproviders'; +import { aqicn, openaq, waqi } from '@shootismoke/dataproviders'; import retry, { Options } from 'async-retry'; import type { Api } from './api'; import { createApi } from './api'; +const AllProviders = ['aqicn', 'openaq', 'waqi'] as const; type AllProviders = 'aqicn' | 'openaq' | 'waqi'; /** @@ -18,14 +19,14 @@ async function providerFetch( ): Promise { const results = provider === 'aqicn' - ? aqicn.normalizeByStation( + ? aqicn.normalize( await aqicn.fetchByStation(station, { token: process.env.BACKEND_AQICN_TOKEN as string, }) ) : provider === 'waqi' - ? waqi.normalizeByStation(await waqi.fetchByStation(station)) - : openaq.normalizeByStation( + ? waqi.normalize(await waqi.fetchByStation(station)) + : openaq.normalize( await openaq.fetchByStation(station, { limit: 1, parameter: ['pm25'], @@ -40,7 +41,7 @@ function assertKnownProvider( provider: string, stationId: string ): asserts provider is AllProviders { - if (!AllProviders.includes(provider)) { + if (!AllProviders.includes(provider as AllProviders)) { throw new Error(`Unrecognized stationId "${stationId}".`); } } diff --git a/packages/ui/src/util/testutil.ts b/packages/ui/src/util/testutil.ts index ffceeab7..031107dc 100644 --- a/packages/ui/src/util/testutil.ts +++ b/packages/ui/src/util/testutil.ts @@ -3,7 +3,7 @@ import { aqicn } from '@shootismoke/dataproviders'; export const testCases = [ { date: '2021-02-28T11:56:06.581Z', - results: aqicn.normalizeByGps( + results: aqicn.normalize( // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access JSON.parse( '{"status":"ok","data":{"aqi":166,"idx":446,"attributions":[{"url":"http://www.bjmemc.com.cn/","name":"Beijing Environmental Protection Monitoring Center (北京市环境保护监测中心)"},{"url":"https://waqi.info/","name":"World Air Quality Index Project"}],"city":{"geo":[39.929,116.417],"name":"Dongcheng Dongsi, Beijing (东城东四)","url":"https://aqicn.org/city/beijing/dongchengdongsi"},"dominentpol":"pm25","iaqi":{"co":{"v":8.2},"dew":{"v":4},"h":{"v":93},"no2":{"v":13.8},"o3":{"v":17.1},"p":{"v":1025},"pm10":{"v":70},"pm25":{"v":166},"so2":{"v":1.6},"t":{"v":5},"w":{"v":0.2}},"time":{"s":"2021-02-28 19:00:00","tz":"+08:00","v":1614538800,"iso":"2021-02-28T19:00:00+08:00"},"forecast":{"daily":{"o3":[{"avg":3,"day":"2021-02-26","max":9,"min":1},{"avg":4,"day":"2021-02-27","max":15,"min":1},{"avg":4,"day":"2021-02-28","max":11,"min":1},{"avg":10,"day":"2021-03-01","max":21,"min":1},{"avg":4,"day":"2021-03-02","max":12,"min":1},{"avg":4,"day":"2021-03-03","max":11,"min":1},{"avg":1,"day":"2021-03-04","max":2,"min":1}],"pm10":[{"avg":100,"day":"2021-02-26","max":122,"min":69},{"avg":97,"day":"2021-02-27","max":122,"min":67},{"avg":105,"day":"2021-02-28","max":122,"min":58},{"avg":48,"day":"2021-03-01","max":60,"min":27},{"avg":66,"day":"2021-03-02","max":73,"min":54},{"avg":120,"day":"2021-03-03","max":126,"min":85},{"avg":101,"day":"2021-03-04","max":122,"min":72},{"avg":59,"day":"2021-03-05","max":77,"min":45},{"avg":50,"day":"2021-03-06","max":57,"min":44}],"pm25":[{"avg":213,"day":"2021-02-26","max":248,"min":169},{"avg":209,"day":"2021-02-27","max":248,"min":167},{"avg":221,"day":"2021-02-28","max":248,"min":158},{"avg":142,"day":"2021-03-01","max":159,"min":87},{"avg":165,"day":"2021-03-02","max":172,"min":154},{"avg":242,"day":"2021-03-03","max":248,"min":170},{"avg":216,"day":"2021-03-04","max":248,"min":169},{"avg":140,"day":"2021-03-05","max":178,"min":93},{"avg":137,"day":"2021-03-06","max":156,"min":102}],"uvi":[{"avg":1,"day":"2021-02-26","max":3,"min":0},{"avg":0,"day":"2021-02-27","max":2,"min":0},{"avg":0,"day":"2021-02-28","max":1,"min":0},{"avg":1,"day":"2021-03-01","max":4,"min":0},{"avg":1,"day":"2021-03-02","max":3,"min":0},{"avg":1,"day":"2021-03-03","max":3,"min":0},{"avg":0,"day":"2021-03-04","max":2,"min":0},{"avg":0,"day":"2021-03-05","max":0,"min":0}]}},"debug":{"sync":"2021-02-28T20:33:16+09:00"}}}' @@ -13,7 +13,7 @@ export const testCases = [ }, { date: '2021-03-01T09:29:07.781Z', - results: aqicn.normalizeByGps( + results: aqicn.normalize( // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access JSON.parse( '{"status":"ok","data":{"aqi":12,"idx":2284,"attributions":[{"url":"http://soramame.taiki.go.jp/","name":"Japan Atmospheric Environmental Regional Observation System (環境省大気汚染物質広域監視システム)","logo":"Japan-Soramame.png"},{"url":"https://www.kankyo.metro.tokyo.lg.jp/","name":"Tokyo, Japan Environment Agency (東京都環境局)","logo":"Japan-Tokyo.png"},{"url":"https://waqi.info/","name":"World Air Quality Index Project"}],"city":{"geo":[35.6718388,139.7551457],"name":"Hibiyakoen, Chiyoda, Tokyo, Japan (日比谷交差点千代田区)","url":"https://aqicn.org/city/japan/chiyodaku/hibiyakosaten"},"dominentpol":"pm10","iaqi":{"co":{"v":3.4},"dew":{"v":8.9},"h":{"v":64},"no2":{"v":26},"o3":{"v":24.1},"p":{"v":1018.4},"pm10":{"v":12},"pm25":{"v":1},"so2":{"v":1.5},"t":{"v":15.7},"w":{"v":6.1}},"time":{"s":"2021-03-01 18:00:00","tz":"+09:00","v":1614621600,"iso":"2021-03-01T18:00:00+09:00"},"forecast":{"daily":{"o3":[{"avg":16,"day":"2021-02-27","max":30,"min":3},{"avg":7,"day":"2021-02-28","max":22,"min":1},{"avg":11,"day":"2021-03-01","max":30,"min":1},{"avg":27,"day":"2021-03-02","max":31,"min":22},{"avg":20,"day":"2021-03-03","max":32,"min":2},{"avg":7,"day":"2021-03-04","max":22,"min":1},{"avg":4,"day":"2021-03-05","max":15,"min":1}],"pm10":[{"avg":6,"day":"2021-02-28","max":9,"min":0},{"avg":31,"day":"2021-03-01","max":46,"min":10},{"avg":12,"day":"2021-03-02","max":24,"min":0},{"avg":8,"day":"2021-03-03","max":17,"min":6},{"avg":18,"day":"2021-03-04","max":19,"min":18},{"avg":22,"day":"2021-03-05","max":26,"min":18},{"avg":27,"day":"2021-03-06","max":57,"min":0},{"avg":10,"day":"2021-03-07","max":17,"min":2}],"pm25":[{"avg":22,"day":"2021-02-28","max":28,"min":13},{"avg":75,"day":"2021-03-01","max":136,"min":20},{"avg":16,"day":"2021-03-02","max":28,"min":0},{"avg":12,"day":"2021-03-03","max":20,"min":0},{"avg":56,"day":"2021-03-04","max":67,"min":28},{"avg":65,"day":"2021-03-05","max":77,"min":42},{"avg":78,"day":"2021-03-06","max":156,"min":20},{"avg":22,"day":"2021-03-07","max":35,"min":13}],"uvi":[{"avg":0,"day":"2021-02-28","max":0,"min":0},{"avg":1,"day":"2021-03-01","max":4,"min":0},{"avg":1,"day":"2021-03-02","max":3,"min":0},{"avg":1,"day":"2021-03-03","max":5,"min":0},{"avg":1,"day":"2021-03-04","max":5,"min":0},{"avg":1,"day":"2021-03-05","max":3,"min":0}]}},"debug":{"sync":"2021-03-01T18:11:20+09:00"}}}' diff --git a/packages/ui/test/e2e/geoapify.spec.ts b/packages/ui/test/e2e/geoapify.spec.ts index a0a58b30..defcac06 100644 --- a/packages/ui/test/e2e/geoapify.spec.ts +++ b/packages/ui/test/e2e/geoapify.spec.ts @@ -1,23 +1,14 @@ -import { pipe } from 'fp-ts/lib/pipeable'; -import * as T from 'fp-ts/lib/Task'; -import * as TE from 'fp-ts/lib/TaskEither'; - import { geoapify } from '../../src/util/geoapify'; describe('geoapify', () => { ['paris', 'beijing', 'dsfewrwea', '123', '!?#'].forEach((searchString) => { - it(`should search for ${searchString}`, () => { - return pipe( - geoapify(searchString, process.env.GEOAPIFY_API_KEY as string), - TE.fold( - (err) => { - throw err; - }, - () => { - return T.of(undefined); - } - ) - )(); + it(`should search for ${searchString}`, async () => { + const hits = await geoapify( + searchString, + process.env.GEOAPIFY_API_KEY as string + ); + + expect(hits).toBeDefined(); }); }); }); diff --git a/yarn.lock b/yarn.lock index 7faf47c3..9fd0267c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -697,7 +697,7 @@ pirates "^4.0.5" source-map-support "^0.5.16" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.18.6", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.8.4": version "7.19.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== @@ -3879,21 +3879,6 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" -create-react-class@^15.7.0: - version "15.7.0" - resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.7.0.tgz#7499d7ca2e69bb51d13faf59bd04f0c65a1d6c1e" - integrity sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng== - dependencies: - loose-envify "^1.3.1" - object-assign "^4.1.1" - -cross-fetch@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== - dependencies: - node-fetch "2.6.7" - cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -3923,14 +3908,6 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -css-in-js-utils@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz#3b472b398787291b47cfe3e44fecfdd9e914ba99" - integrity sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA== - dependencies: - hyphenate-style-name "^1.0.2" - isobject "^3.0.1" - csstype@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" @@ -4755,19 +4732,6 @@ fbjs@^1.0.0: setimmediate "^1.0.5" ua-parser-js "^0.7.18" -fbjs@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-3.0.4.tgz#e1871c6bd3083bac71ff2da868ad5067d37716c6" - integrity sha512-ucV0tDODnGV3JCnnkmoszb5lf4bNpzjv80K41wd4k798Etq+UYD0y0TIfalLjZoKgjive6/adkRnszwapiDgBQ== - dependencies: - cross-fetch "^3.1.5" - fbjs-css-vars "^1.0.0" - loose-envify "^1.0.0" - object-assign "^4.1.0" - promise "^7.1.1" - setimmediate "^1.0.5" - ua-parser-js "^0.7.30" - figures@3.2.0, figures@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -4900,11 +4864,6 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -fp-ts@^2.12.3: - version "2.12.3" - resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.12.3.tgz#d991b1e8467d325dadbb6b0ab9524f773e9c3c49" - integrity sha512-8m0XvW8kZbfnJOA4NvSVXu95mLbPf4LQGwQyqVukIYS4KzSNJiyKSmuZUmbVHteUi6MGkAJGPb0goPZqI+Tsqg== - fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -5401,11 +5360,6 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -hyphenate-style-name@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" - integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== - iconv-lite@^0.4.17, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -5517,13 +5471,6 @@ init-package-json@^3.0.2: validate-npm-package-license "^3.0.4" validate-npm-package-name "^4.0.0" -inline-style-prefixer@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-6.0.1.tgz#c5c0e43ba8831707afc5f5bbfd97edf45c1fa7ae" - integrity sha512-AsqazZ8KcRzJ9YPN1wMH2aNM7lkWQ8tSPrW5uDk1ziYwiAPWSZnUsC7lfZq+BDqLqz0B4Pho5wscWcJzVvRzDQ== - dependencies: - css-in-js-utils "^2.0.0" - inquirer@^3.0.6: version "3.3.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" @@ -5581,11 +5528,6 @@ invariant@^2.2.2, invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -io-ts@^2.2.18: - version "2.2.18" - resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.2.18.tgz#dfb41aded5f0e598ccf2a25a483c205444210173" - integrity sha512-3JxUUzRtPQPs5sOwB7pW0+Xb54nOzqA6M1sRB1hwgsRmkWMeGTjtOrCynGTJhIj+mBLUj2S37DAq2+BrPh9kTQ== - ip@^1.1.5: version "1.1.8" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48" @@ -6783,7 +6725,7 @@ logkitty@^0.7.1: dayjs "^1.8.15" yargs "^15.1.0" -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -7496,13 +7438,6 @@ node-addon-api@^3.2.1: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== -node-fetch@2.6.7, node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - node-fetch@^1.0.1: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" @@ -7511,6 +7446,13 @@ node-fetch@^1.0.1: encoding "^0.1.11" is-stream "^1.0.1" +node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + node-gyp-build@^4.3.0: version "4.5.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40" @@ -7554,11 +7496,6 @@ nopt@^5.0.0: dependencies: abbrev "1" -normalize-css-color@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/normalize-css-color/-/normalize-css-color-1.0.2.tgz#02991e97cccec6623fe573afbbf0de6a1f3e9f8d" - integrity sha512-jPJ/V7Cp1UytdidsPqviKEElFQJs22hUUgK5BOPHTwOonNCk7/2qOxhhqzEajmFrWJowADFfOFh1V+aWkRfy+w== - normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -8301,11 +8238,6 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg== -postcss-value-parser@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" - integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -8501,19 +8433,6 @@ react-native-safe-modules@^1.0.3: dependencies: dedent "^0.6.0" -react-native-web@^0.18.9: - version "0.18.9" - resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.18.9.tgz#f5032e0b32ebe99c0ab22d13dbd8ca2944b08f12" - integrity sha512-BaV5Mpe7u9pN5vTRDW2g+MLh6PbPBJZpXRQM3Jr2cNv7hNa3sxCGh9T+NcW6wOFzf/+USrdrEPI1M9wNyr7vyA== - dependencies: - "@babel/runtime" "^7.18.6" - create-react-class "^15.7.0" - fbjs "^3.0.4" - inline-style-prefixer "^6.0.1" - normalize-css-color "^1.0.2" - postcss-value-parser "^4.2.0" - styleq "^0.1.2" - "react-native@https://github.com/expo/react-native/archive/sdk-40.0.0.tar.gz": version "0.63.2" resolved "https://github.com/expo/react-native/archive/sdk-40.0.0.tar.gz#b16b76e81617396a5c1e0f3e44a9553e8bead8cd" @@ -8837,11 +8756,6 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -retry-ts@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/retry-ts/-/retry-ts-0.1.4.tgz#b60c4e1580e11304bc8af90d1b1a10cf651d70f8" - integrity sha512-qUfPfEU+EjUVNU0aTDFjLtFJAh0yRD8TixwMs06WY/L8rTwUTluegfhT1k+9d4IKxa0XJ8r86+yxq/oxdx4fgg== - retry@0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" @@ -9538,11 +9452,6 @@ strong-log-transformer@^2.1.0: minimist "^1.2.0" through "^2.3.4" -styleq@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/styleq/-/styleq-0.1.2.tgz#052b46af5ca4f920b1bdae2735ffb1e3970f53cd" - integrity sha512-EBNuMVSxpssuFcJq/c4zmZ4tpCyX9E27hz5xPJhw4URjRHcYXPHh8rDHY/tJsw5gtP0+tIL3IBYeQVIYjdZFhg== - sudo-prompt@^9.0.0: version "9.2.1" resolved "https://registry.yarnpkg.com/sudo-prompt/-/sudo-prompt-9.2.1.tgz#77efb84309c9ca489527a4e749f287e6bdd52afd" @@ -9887,7 +9796,7 @@ typedoc@^0.23.15: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== -ua-parser-js@^0.7.18, ua-parser-js@^0.7.30: +ua-parser-js@^0.7.18: version "0.7.31" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ== From a108a8ebe3dad3b61ceff6e4f6b5043beb7e84ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 Feb 2023 15:04:24 +0000 Subject: [PATCH 2/2] chore(deps): Bump json5 from 1.0.1 to 1.0.2 Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2. - [Release notes](https://github.com/json5/json5/releases) - [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md) - [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2) --- updated-dependencies: - dependency-name: json5 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9fd0267c..693d6b17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6443,9 +6443,9 @@ json-stringify-safe@^5.0.1: integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" @@ -7205,9 +7205,9 @@ minimist-options@4.1.0: kind-of "^6.0.3" minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== minipass-collect@^1.0.2: version "1.0.2"