diff --git a/src/components/charts/CategoryExpenses.tsx b/src/components/charts/CategoryExpenses.tsx
index 04913ff..89b5f22 100644
--- a/src/components/charts/CategoryExpenses.tsx
+++ b/src/components/charts/CategoryExpenses.tsx
@@ -3,20 +3,8 @@ import CategorySelect from '../shared/CategorySelect';
import CategoryTreeMap from './CategoryTreeMap';
import CategoryLine from './CategoryLine';
import { Moment } from 'moment';
-import { Transaction, Category } from '../../types/redux';
-
-interface CategoryOption {
- label: string;
- value: string;
-}
-
-interface CategoryExpense {
- value: {
- amount: number;
- category: Category;
- transactions: Transaction[];
- };
-}
+import { Category } from '../../types/redux';
+import { type CategoryOption, type CategoryExpense } from '../../types/app';
interface Props {
handleCategoryChange: (categoryIds: string[]) => void;
diff --git a/src/components/charts/CategoryLine.tsx b/src/components/charts/CategoryLine.tsx
index ff9c694..76d30c3 100644
--- a/src/components/charts/CategoryLine.tsx
+++ b/src/components/charts/CategoryLine.tsx
@@ -2,15 +2,8 @@ import React from 'react';
import { schemeCategory10 } from 'd3-scale-chromatic';
import { Moment } from 'moment';
import CustomLineChart from '../shared/CustomLineChart';
-import { Transaction, Category } from '../../types/redux';
-
-interface CategoryExpense {
- value: {
- amount: number;
- category: Category;
- transactions: Transaction[];
- };
-}
+import { Transaction } from '../../types/redux';
+import { type CategoryExpense } from '../../types/app';
interface Props {
sortedCategoryExpenses: CategoryExpense[];
diff --git a/src/components/charts/CategoryTreeMap.tsx b/src/components/charts/CategoryTreeMap.tsx
index b2a8c54..7066bc2 100644
--- a/src/components/charts/CategoryTreeMap.tsx
+++ b/src/components/charts/CategoryTreeMap.tsx
@@ -6,14 +6,7 @@ import {
} from 'recharts';
import color from '../../data/color';
import { formatNumber } from '../../util';
-import { Category } from '../../types/redux';
-
-interface CategoryExpense {
- value: {
- amount: number;
- category: Category;
- };
-}
+import { type CategoryExpense } from '../../types/app';
interface Props {
sortedCategoryExpenses: CategoryExpense[];
diff --git a/src/components/charts/IncomeExpensesLine.tsx b/src/components/charts/IncomeExpensesLine.tsx
index 3da2572..4b09edd 100644
--- a/src/components/charts/IncomeExpensesLine.tsx
+++ b/src/components/charts/IncomeExpensesLine.tsx
@@ -5,6 +5,7 @@ import color from '../../data/color';
import CustomLineChart from '../shared/CustomLineChart';
import { Moment } from 'moment';
import { Transaction } from '../../types/redux';
+import { type NestEntry } from '../../types/app';
interface Props {
transactions: Transaction[];
@@ -23,20 +24,20 @@ export default function IncomeExpensesLine({ transactions, startDate, endDate }:
const keySelector = endDate.diff(startDate, 'month', true) < 2 ?
(d: Transaction) => d.date :
(d: Transaction) => d.date.substring(0, 7)
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const data = (nest() as any)
+
+ const data = nest
()
.key(keySelector)
.sortKeys(ascending)
+ // @ts-expect-error d3-collection types: .key() this-return doesn't track .rollup() generic
.rollup((a: Transaction[]) => {
return {
expenses: Math.abs(sum(a, d => (d.amount < 0 ? d.amount : 0))),
income: sum(a, d => (d.amount >= 0 ? d.amount : 0))
}
})
- .entries(transactions);
+ .entries(transactions) as unknown as NestEntry<{ expenses: number; income: number }>[];
- const processedData: ProcessedData[] = data.map((d: { key: string; value: { expenses: number; income: number } }) => ({
+ const processedData: ProcessedData[] = data.map((d) => ({
key: d.key,
expenses: d.value.expenses,
income: d.value.income
diff --git a/src/components/charts/Summary.tsx b/src/components/charts/Summary.tsx
index 01fd4a4..bca3945 100644
--- a/src/components/charts/Summary.tsx
+++ b/src/components/charts/Summary.tsx
@@ -3,16 +3,8 @@ import { nest } from 'd3-collection';
import { sum } from 'd3-array';
import { formatNumber } from '../../util';
import AmountCard from './AmountCard';
-import { Transaction, Account, Category } from '../../types/redux';
-
-interface CategoryExpense {
- key: string;
- value: {
- amount: number;
- transactions: Transaction[];
- category: Category;
- };
-}
+import { Transaction, Account } from '../../types/redux';
+import { type CategoryExpense, type NestEntry } from '../../types/app';
interface SummaryProps {
expenses: Transaction[];
@@ -22,15 +14,15 @@ interface SummaryProps {
}
const groupAmounts = (transactions: Transaction[], accounts: { [key: string]: Account }) => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- return (nest() as any)
+ return (nest()
.key((t: Transaction) => t.account || '')
+ // @ts-expect-error d3-collection types: .key() this-return doesn't track .rollup() generic
.rollup((t: Transaction[]) => ({
amount: sum(t, d => d.amount),
originalAmount: sum(t, d => (d as unknown as Record).originalAmount || 0)
}))
- .entries(transactions)
- .map((e: { key: string; value: { amount: number; originalAmount: number } }) => ({ account: accounts[e.key], amounts: e.value }));
+ .entries(transactions) as unknown as NestEntry<{ amount: number; originalAmount: number }>[])
+ .map((e) => ({ account: accounts[e.key], amounts: e.value }));
};
export default function Summary({ expenses, income, sortedCategoryExpenses, accounts }: SummaryProps) {
diff --git a/src/components/shared/CategorySelect.tsx b/src/components/shared/CategorySelect.tsx
index 3e3a5da..6898189 100644
--- a/src/components/shared/CategorySelect.tsx
+++ b/src/components/shared/CategorySelect.tsx
@@ -5,11 +5,7 @@ import CreatableSelect from 'react-select/creatable';
import { uncategorized } from '../../data/categories';
import { arrayToObjectLookup } from '../../util';
import type { RootState } from '../../reducers';
-
-interface CategoryOption {
- label: string;
- value: string;
-}
+import type { CategoryOption } from '../../types/app';
interface Props {
onChange: (option: CategoryOption[] | CategoryOption | null) => void;
@@ -22,14 +18,20 @@ interface Props {
selectOptions?: Record;
}
-const findValue = (value: Set | string[] | string | CategoryOption | CategoryOption[] | null | undefined, options: CategoryOption[]) => {
+const findValue = (
+ value: Set | string[] | string | CategoryOption | CategoryOption[] | null | undefined,
+ options: CategoryOption[]
+): CategoryOption | CategoryOption[] | null | undefined => {
const normalizedValue = value instanceof Set ? Array.from(value) : value;
// If we are given an array of strings, select the options with the value of
// those strings.
- if (Array.isArray(normalizedValue) && normalizedValue.length && typeof normalizedValue[0] === 'string') {
- const stringValues = normalizedValue as string[];
- return options.filter(o => !!stringValues.find((c: string) => c === o.value));
+ if (Array.isArray(normalizedValue)) {
+ if (normalizedValue.length && typeof normalizedValue[0] === 'string') {
+ const stringValues = normalizedValue as string[];
+ return options.filter(o => !!stringValues.find((c: string) => c === o.value));
+ }
+ return normalizedValue as CategoryOption[];
}
if (typeof normalizedValue === 'string') {
return options.find(o => o.value === normalizedValue);
@@ -70,8 +72,9 @@ export default function CategorySelect({
].concat(categoryOptions);
}, [categories]);
- const onChangeInternal = (option: CategoryOption | CategoryOption[] | null) => {
- onChange(option);
+ const onChangeInternal = (option: CategoryOption | readonly CategoryOption[] | null) => {
+ // Convert readonly arrays from react-select to mutable arrays for our onChange prop
+ onChange(Array.isArray(option) ? [...option] : option as CategoryOption | null);
};
const selectedCategoryValue = useMemo(
@@ -79,15 +82,13 @@ export default function CategorySelect({
[selectedCategory, categoryOptionsWithUncategorized]
);
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const commonProps: any = {
+ const baseProps = {
options: categoryOptionsWithUncategorized,
placeholder: placeholder,
onChange: onChangeInternal,
value: selectedCategoryValue,
isMulti: isMulti,
autoFocus: focus,
- ...selectOptions
};
// Assume it's a creatable if the onCreate function is given
@@ -100,7 +101,8 @@ export default function CategorySelect({
className="category-select creatable"
onCreateOption={onCreateOption}
- {...commonProps}
+ {...selectOptions}
+ {...baseProps}
/>
);
}
@@ -108,7 +110,8 @@ export default function CategorySelect({
return (
);
}
diff --git a/src/components/transactions/BulkActionSelection.tsx b/src/components/transactions/BulkActionSelection.tsx
index 6393c69..5858594 100644
--- a/src/components/transactions/BulkActionSelection.tsx
+++ b/src/components/transactions/BulkActionSelection.tsx
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import Select from 'react-select';
-import { Transaction } from '../../types/redux';
+import { type CategoryOption, type DisplayTransaction } from '../../types/app';
import CategorySelect from '../shared/CategorySelect';
interface BulkAction {
@@ -8,17 +8,12 @@ interface BulkAction {
label: string;
}
-interface CategoryOption {
- value: string;
- label: string;
-}
-
const CONFIRM_CATEGORY_GUESSES = 'confirmCategoryGuesses';
const SET_CATEGORIES = 'setCategories';
const GROUP_TRANSACTIONS = 'groupRows';
interface BulkActionSelectionProps {
- selectedTransactions: Transaction[];
+ selectedTransactions: DisplayTransaction[];
handleRowCategoryChange: (mapping: { [transactionId: string]: string }) => void;
showCreateCategoryModal: () => void;
handleGroupRows: (transactionIds: string[]) => void;
diff --git a/src/components/transactions/BulkActions.tsx b/src/components/transactions/BulkActions.tsx
index 9644c34..6d7151f 100644
--- a/src/components/transactions/BulkActions.tsx
+++ b/src/components/transactions/BulkActions.tsx
@@ -1,9 +1,9 @@
import React from 'react';
-import { Transaction } from '../../types/redux';
+import { type DisplayTransaction } from '../../types/app';
import BulkActionSelection from './BulkActionSelection';
interface SelectionIntroProps {
- selectedTransactions: Transaction[];
+ selectedTransactions: DisplayTransaction[];
handleSelectAll: () => void;
handleSelectNone: () => void;
}
@@ -31,7 +31,7 @@ function SelectionIntro({
}
interface BulkActionsProps {
- selectedTransactions: Transaction[];
+ selectedTransactions: DisplayTransaction[];
handleRowCategoryChange: (mapping: { [transactionId: string]: string }) => void;
handleSelectAll: () => void;
handleSelectNone: () => void;
diff --git a/src/components/transactions/GroupedTransaction.tsx b/src/components/transactions/GroupedTransaction.tsx
index 0f31671..e5137ad 100644
--- a/src/components/transactions/GroupedTransaction.tsx
+++ b/src/components/transactions/GroupedTransaction.tsx
@@ -1,20 +1,11 @@
import React from 'react';
import { Tooltip } from 'react-tooltip';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Moment } from 'moment';
-
-interface TransactionGroup {
- groupId: string;
- linkedTransactions: Array<{
- id: string;
- date: Moment;
- description: string;
- }>;
-}
+import { type DisplayTransactionGroup } from '../../types/app';
interface GroupedTransactionProps {
transactionId: string;
- transactionGroup?: TransactionGroup;
+ transactionGroup?: DisplayTransactionGroup;
handleDeleteTransactionGroup: (groupId: string) => void;
}
diff --git a/src/components/transactions/RowCategorizer.tsx b/src/components/transactions/RowCategorizer.tsx
index 127a549..2662dee 100644
--- a/src/components/transactions/RowCategorizer.tsx
+++ b/src/components/transactions/RowCategorizer.tsx
@@ -1,17 +1,12 @@
import React, { useState, useEffect } from 'react';
-import { Transaction } from '../../types/redux';
+import { type CategoryOption, type DisplayTransaction } from '../../types/app';
import Category from './Category';
import CategorySelect from '../shared/CategorySelect';
-interface CategoryOption {
- label: string;
- value: string;
-}
-
interface RowCategorizerProps {
handleRowCategoryChange: (mapping: { [transactionId: string]: string }) => void;
showCreateCategoryModal: (name: string, transactionId: string) => void;
- transaction: Transaction;
+ transaction: DisplayTransaction;
}
export default function RowCategorizer({
diff --git a/src/components/transactions/TransactionRow.tsx b/src/components/transactions/TransactionRow.tsx
index ac5aaac..0a24cb4 100644
--- a/src/components/transactions/TransactionRow.tsx
+++ b/src/components/transactions/TransactionRow.tsx
@@ -1,8 +1,9 @@
import React, { useState } from 'react';
-import moment, { Moment } from 'moment';
+import moment from 'moment';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from 'react-tooltip';
-import { Transaction, Account } from '../../types/redux';
+import { Account } from '../../types/redux';
+import { type DisplayTransactionGroup, type DisplayTransaction } from '../../types/app';
import ConfirmModal from '../shared/ConfirmModal';
import RowCategorizer from './RowCategorizer';
import IgnoreTransaction from './IgnoreTransaction';
@@ -10,15 +11,6 @@ import GroupedTransaction from './GroupedTransaction';
import ConfirmDelete from './ConfirmDelete';
import { formatNumber } from '../../util';
-interface TransactionGroup {
- groupId: string;
- linkedTransactions: Array<{
- id: string;
- date: Moment;
- description: string;
- }>;
-}
-
interface AmountProps {
amount: number | string;
hasMultipleAccounts: boolean;
@@ -76,7 +68,7 @@ function Amount({
}
interface TransactionRowProps {
- transaction: Transaction;
+ transaction: DisplayTransaction;
accounts: { [accountId: string]: Account };
showCreateCategoryModal: (name: string, transactionId: string) => void;
handleDeleteRow: (transactionId: string) => void;
@@ -85,7 +77,7 @@ interface TransactionRowProps {
handleRowSelect: (transactionId: string) => void;
roundAmount: boolean;
handleDeleteTransactionGroup: (groupId: string) => void;
- transactionGroup?: TransactionGroup;
+ transactionGroup?: DisplayTransactionGroup;
isSelected?: boolean;
}
diff --git a/src/components/transactions/TransactionTable.tsx b/src/components/transactions/TransactionTable.tsx
index 8192c0f..85e9a2a 100644
--- a/src/components/transactions/TransactionTable.tsx
+++ b/src/components/transactions/TransactionTable.tsx
@@ -9,9 +9,8 @@ import TransactionRow from './TransactionRow';
import SortHeader from './SortHeader';
import SearchField from './SearchField';
import BulkActions from './BulkActions';
-import { Transaction } from '../../types/redux';
import type { Category, Account } from '../../types/redux';
-
+import type { DisplayTransactionGroup, DisplayTransaction } from '../../types/app';
interface DateSelect {
id: string;
@@ -20,7 +19,7 @@ interface DateSelect {
}
interface Props {
- transactions: Transaction[];
+ transactions: DisplayTransaction[];
categories: Record;
accounts: Record;
dateSelect: DateSelect;
@@ -33,7 +32,7 @@ interface Props {
handleIgnoreRow: (transactionId: string, ignored: boolean) => void;
handleDeleteRow: (transactionId: string) => void;
handleGroupRows: (transactionIds: string[]) => void;
- handleRowCategoryChange: (mapping: { [transactionId: string]: string }) => void;
+ handleRowCategoryChange: (mapping: Record) => void;
handleSearch: (text: string, page: number) => void;
handleDatesChange: (id: string, startDate: Moment | null, endDate: Moment | null) => void;
handlePageChange: (page: number) => void;
@@ -44,13 +43,12 @@ interface Props {
handleRoundAmount: (round: boolean) => void;
handleDeleteTransactionGroup: (groupId: string) => void;
roundAmount?: boolean;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- transactionGroups?: Record;
+ transactionGroups?: Record;
}
-const filterData = (data: Transaction[], categories: Set, dateSelect: DateSelect): Transaction[] => {
- let categoryFilter: (t: Transaction) => boolean = () => true;
- let dateFilter: (t: Transaction) => boolean = () => true;
+const filterData = (data: DisplayTransaction[], categories: Set, dateSelect: DateSelect): DisplayTransaction[] => {
+ let categoryFilter: (t: DisplayTransaction) => boolean = () => true;
+ let dateFilter: (t: DisplayTransaction) => boolean = () => true;
if (categories && categories.size > 0) {
categoryFilter = t => {
@@ -69,7 +67,7 @@ const filterData = (data: Transaction[], categories: Set, dateSelect: Da
return data.filter(t => categoryFilter(t) && dateFilter(t));
};
-const sortData = (data: Transaction[], sortKey: string, sortAscending: boolean): Transaction[] => {
+const sortData = (data: DisplayTransaction[], sortKey: string, sortAscending: boolean): DisplayTransaction[] => {
return [...data] // Create new array to avoid inplace sort of original array.
.sort((a, b) => {
const val1 = (a as unknown as Record)[sortKey];
@@ -110,8 +108,8 @@ export default function TransactionTable({
transactionGroups = {}
}: Props) {
const [selectedRows, setSelectedRows] = useState>(new Set());
- const [data, setData] = useState([]);
- const [dataView, setDataView] = useState([]);
+ const [data, setData] = useState([]);
+ const [dataView, setDataView] = useState([]);
// Note: react-tooltip v5 doesn't need manual rebuild
diff --git a/src/configureStore.ts b/src/configureStore.ts
index c73c489..6139f70 100644
--- a/src/configureStore.ts
+++ b/src/configureStore.ts
@@ -3,7 +3,7 @@ import { createLogger } from 'redux-logger';
import { persistStore, persistReducer } from 'redux-persist';
import { reduxSearch, SearchApi } from 'redux-search';
import storage from 'redux-persist/lib/storage';
-import rootReducer from './reducers'
+import rootReducer, { type RootState } from './reducers'
export const persistConfig = {
key: 'otb',
@@ -18,31 +18,35 @@ export const persistConfig = {
const persistedReducer = persistReducer(persistConfig, rootReducer);
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export default function configureStore(preloadedState: any = {}) {
+type PersistedState = RootState & { _persist: { version: number; rehydrated: boolean } };
+
+export default function configureStore(preloadedState: Partial = {}) {
const store = createStore({
reducer: persistedReducer,
- preloadedState,
- middleware: (getDefaultMiddleware) =>
- getDefaultMiddleware({
+ preloadedState: preloadedState as PersistedState,
+ middleware: (getDefaultMiddleware) => {
+ const middleware = getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE', 'persist/FLUSH', 'persist/PURGE'],
// Ignore these field paths in the state - they are now arrays, not Sets
ignoredPaths: ['edit.transactionList.filterCategories', 'edit.charts.filterCategories']
}
- }).concat(
- process.env.NODE_ENV === 'development' ? [createLogger()] : []
- ),
+ });
+ if (process.env.NODE_ENV === 'development') {
+ return middleware.concat(createLogger());
+ }
+ return middleware;
+ },
enhancers: (getDefaultEnhancers) =>
getDefaultEnhancers().concat([
reduxSearch({
resourceIndexes: {
transactions: ['description', 'descriptionCleaned']
},
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- resourceSelector: (resourceName: string, state: any) => {
+ resourceSelector: (resourceName: string, state: unknown) => {
+ const typedState = state as RootState;
if (resourceName === 'transactions') {
- return state.transactions.data;
+ return typedState.transactions.data;
}
return [];
},
diff --git a/src/ml.ts b/src/ml.ts
index afb2188..8d29ebb 100644
--- a/src/ml.ts
+++ b/src/ml.ts
@@ -1,6 +1,7 @@
import bayes from 'bayes';
import { reverseIndexLookup } from './util';
import { type Transaction } from './types/redux';
+import logger from './utils/logger';
interface CategorizerConfig {
bayes: string;
@@ -24,7 +25,7 @@ const retrainBayes = async (transactions: Transaction[]): Promise
});
}
}
diff --git a/src/types/app.ts b/src/types/app.ts
index cb7b47a..85c002b 100644
--- a/src/types/app.ts
+++ b/src/types/app.ts
@@ -1,6 +1,7 @@
// Central location for all application types and interfaces
import { type Account } from '../data/accounts';
import { type Category } from '../data/categories';
+import { type Moment } from 'moment';
// Re-export imported types for convenience
export { type Account, type Category };
@@ -103,4 +104,48 @@ export interface EditState {
charts: ChartsState;
currencies?: string[];
currencyRates?: Record>;
+}
+
+// Shared UI types used across multiple components
+
+// D3 nest entry helper type for typed nest operations
+export interface NestEntry {
+ key: string;
+ value: T;
+}
+
+// Category option for react-select dropdowns (used in CategorySelect, CategoryExpenses, RowCategorizer, BulkActionSelection)
+export interface CategoryOption {
+ label: string;
+ value: string;
+}
+
+// Category expense data used across chart components (Charts, Summary, CategoryExpenses, CategoryLine, CategoryTreeMap)
+export interface CategoryExpense {
+ key: string;
+ value: {
+ amount: number;
+ transactions: Transaction[];
+ category: Category;
+ };
+}
+
+// Enriched transaction for display (with resolved categories and Moment date)
+export interface DisplayTransaction extends Omit {
+ date: Moment;
+ categoryGuess: Category | null;
+ categoryConfirmed: Category | null;
+}
+
+// Linked transaction summary for display in grouped transaction tooltips
+export interface LinkedTransaction {
+ id: string;
+ date: Moment;
+ description: string;
+}
+
+// Transaction group with resolved linked transactions for display
+export interface DisplayTransactionGroup {
+ groupId: string;
+ linkedTransactions: LinkedTransaction[];
}
\ No newline at end of file
diff --git a/src/types/redux-search.d.ts b/src/types/redux-search.d.ts
index 5fdfaaf..18c394a 100644
--- a/src/types/redux-search.d.ts
+++ b/src/types/redux-search.d.ts
@@ -1,11 +1,32 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
declare module 'redux-search' {
- export function reducer(state: any, action: any): any;
- export function connectSearchBox(reducer: any, stateKey: string): any;
- export function connectSearchBox(reducer: any, stateKey: string, searchStateSelector?: any): any;
- export function createSearchAction(resourceName: string): (searchText: string) => any;
- export function reduxSearch(config: any): any;
+ import { Action, StoreEnhancer } from 'redux';
+
+ export interface SearchState {
+ [resourceName: string]: {
+ text: string;
+ result: string[];
+ };
+ }
+
+ export function reducer(state: SearchState | undefined, action: Action): SearchState;
+
+ export function createSearchAction(resourceName: string): (searchText: string) => Action;
+
+ export interface ReduxSearchConfig {
+ resourceIndexes: Record;
+ resourceSelector: (resourceName: string, state: unknown) => unknown[];
+ searchApi?: SearchApi;
+ }
+
+ export function reduxSearch(config: ReduxSearchConfig): StoreEnhancer;
+
+ export interface SearchApiOptions {
+ matchAnyToken?: boolean;
+ indexMode?: string;
+ tokenizeString?: (text: string) => string[];
+ }
+
export class SearchApi {
- constructor(options: any);
+ constructor(options?: SearchApiOptions);
}
}
diff --git a/src/utils/eurofx/index.ts b/src/utils/eurofx/index.ts
index 0a4af74..84163f0 100644
--- a/src/utils/eurofx/index.ts
+++ b/src/utils/eurofx/index.ts
@@ -2,6 +2,7 @@ import axios from 'axios';
import * as Papa from 'papaparse';
import { LRUCache } from 'lru-cache';
import { unzip } from './zip';
+import logger from '../logger';
const rateDailyUrl = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref.zip';
const rateHistoricalUrl = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.zip';
@@ -17,11 +18,11 @@ const cache = new LRUCache>>({
const fetchRatesCsv = async (url: string): Promise>> => {
const cached = cache.get(url);
if (cached) {
- console.log('Using cached CSV file');
+ logger.debug('Using cached CSV file');
return cached;
}
- console.log(`Fetching CSV from ${url}`)
+ logger.debug(`Fetching CSV from ${url}`)
const resp = await axios.get(url, {
responseType: 'arraybuffer'
@@ -50,7 +51,7 @@ interface FetchRatesOptions {
* @returns {Array} A list of exchange rates (one per day)
*/
export const fetchRates = async (options: FetchRatesOptions = {}): Promise[]> => {
- console.log('Got options', options);
+ logger.debug('Got options', options);
const url = options.historical ? rateHistoricalUrl : rateDailyUrl;
@@ -76,4 +77,4 @@ export const fetchRates = async (options: FetchRatesOptions = {}): Promise void) => {
- return (...args: unknown[]) => {
- return new Promise((resolve, reject) => {
- api(...args, (err: Error | null, response: unknown) => {
- if (err) return reject(err);
- resolve(response);
- });
+const yauzlFromBuffer = (data: ArrayBuffer, options: yauzl.Options): Promise => {
+ return new Promise((resolve, reject) => {
+ yauzl.fromBuffer(Buffer.from(data), options, (err: Error | null, zipfile?: yauzl.ZipFile) => {
+ if (err) return reject(err);
+ resolve(zipfile!);
});
- };
+ });
};
-const yauzlFromBuffer = promisify(yauzl.fromBuffer);
-
/**
* Unzip the data in the given buffer
* @param {Buffer} data - The zipfile to unzip as a buffer.
@@ -22,11 +18,18 @@ const yauzlFromBuffer = promisify(yauzl.fromBuffer);
*/
export const unzip = async (data: ArrayBuffer): Promise> => {
// Prepare the zip file
- const zipFile = await yauzlFromBuffer(data, { lazyEntries: true }) as yauzl.ZipFile;
- console.log('Number of entries in zipfile:', zipFile.entryCount);
+ const zipFile = await yauzlFromBuffer(data, { lazyEntries: true });
+ logger.debug('Number of entries in zipfile:', zipFile.entryCount);
// Open a read stream
- const openReadStream = promisify(zipFile.openReadStream.bind(zipFile));
+ const openReadStream = (entry: yauzl.Entry): Promise => {
+ return new Promise((resolve, reject) => {
+ zipFile.openReadStream(entry, (err: Error | null, stream?: Readable) => {
+ if (err) return reject(err);
+ resolve(stream!);
+ });
+ });
+ };
// Read all entries and return each csv file.
return new Promise((resolve, reject) => {
@@ -34,14 +37,14 @@ export const unzip = async (data: ArrayBuffer): Promise>
zipFile.readEntry();
zipFile.on('entry', async (entry: yauzl.Entry) => {
- console.log('Reading file:', entry.fileName);
+ logger.debug('Reading file:', entry.fileName);
files[entry.fileName] = []; // Create an empty array to gather the chunks
- const stream = await openReadStream(entry) as Readable;
+ const stream = await openReadStream(entry);
stream.on('end', () => {
- console.log('Done with:', entry.fileName);
+ logger.debug('Done with:', entry.fileName);
// Gather all the chunks into a string
files[entry.fileName] = Buffer.concat(files[entry.fileName] as Buffer[]).toString();
@@ -61,4 +64,4 @@ export const unzip = async (data: ArrayBuffer): Promise>
zipFile.on('error', reject);
});
-};
\ No newline at end of file
+};
diff --git a/src/utils/logger.ts b/src/utils/logger.ts
new file mode 100644
index 0000000..8b73044
--- /dev/null
+++ b/src/utils/logger.ts
@@ -0,0 +1,5 @@
+import log from 'loglevel';
+
+log.setLevel((process.env.NEXT_PUBLIC_LOG_LEVEL || 'INFO') as log.LogLevelDesc);
+
+export default log;