Migrate core to TS p3 (#896)

Another batch of `loot-core` migrated.
This commit is contained in:
Alberto Gasparin
2023-04-23 10:43:47 +10:00
committed by GitHub
parent b0c5a9389c
commit e8a62f89a1
143 changed files with 1257 additions and 257 deletions

View File

@@ -1,4 +1,3 @@
{
"presets": ["@babel/preset-env", "@babel/preset-typescript"],
"ignore": ["**/__mocks__"]
"presets": ["@babel/preset-env", "@babel/preset-typescript"]
}

View File

@@ -42,9 +42,11 @@
"@babel/core": "~7.14.3",
"@babel/preset-env": "^7.20.2",
"@babel/preset-typescript": "^7.20.2",
"@types/better-sqlite3": "^7.6.4",
"@types/jest": "^27.5.0",
"@types/jlongster__sql.js": "npm:@types/sql.js@latest",
"@types/node-ipc": "^9.2.0",
"@types/pegjs": "^0.10.3",
"@types/webpack": "^5.28.0",
"adm-zip": "^0.5.9",
"babel-loader": "^8.0.6",
@@ -67,6 +69,7 @@
"throttleit": "^1.0.0",
"ts-jest": "^27.0.0",
"ts-node": "^10.7.0",
"ts-protoc-gen": "^0.15.0",
"typescript": "^4.6.4",
"uuid": "3.3.2",
"webpack": "^4.41.2",

View File

@@ -227,12 +227,3 @@ export function markAccountRead(accountId) {
accountId: accountId,
};
}
export function getBanks() {
return async dispatch => {
dispatch({
type: constants.LOAD_BANKS,
banks: await send('banks'),
});
};
}

View File

@@ -130,7 +130,7 @@ export function deleteBudget(id, cloudFileId) {
};
}
export function createBudget({ testMode, demoMode } = {}) {
export function createBudget({ testMode = false, demoMode = false } = {}) {
return async (dispatch, getState) => {
dispatch(
setAppState({
@@ -165,6 +165,7 @@ export function importBudget(filepath, type) {
dispatch(closeModal());
await dispatch(loadPrefs());
// @ts-expect-error __history needs refinement
window.__history.push('/budget');
};
}
@@ -193,7 +194,7 @@ export function closeAndDownloadBudget(cloudFileId) {
};
}
export function downloadBudget(cloudFileId, { replace } = {}) {
export function downloadBudget(cloudFileId, { replace = false } = {}) {
return async dispatch => {
dispatch(setAppState({ loadingText: 'Downloading...' }));

View File

@@ -1,21 +1,10 @@
import { send } from '../../platform/client/fetch';
import { getUploadError } from '../../shared/errors';
import * as constants from '../constants';
import { syncAccounts } from './account';
import { pushModal } from './modals';
import { loadPrefs } from './prefs';
export function unregister() {
return async dispatch => {
const profile = await send('unregister');
dispatch({
type: constants.SET_PROFILE,
profile,
});
};
}
export function resetSync() {
return async (dispatch, getState) => {
let { error } = await send('sync-reset');

View File

@@ -1,12 +0,0 @@
import { send } from '../../platform/client/fetch';
import { filterTransactions } from './queries';
export function categorize(accountId) {
return async dispatch => {
const res = await send('transactions-categorize', { accountId });
if (res === 'ok') {
dispatch(filterTransactions(null, accountId));
}
};
}

View File

@@ -34,7 +34,7 @@ export function CachedAccounts({ children, idKey }) {
return children(data);
}
export function useCachedAccounts({ idKey } = {}) {
export function useCachedAccounts({ idKey }: { idKey? } = {}) {
let data = useContext(AccountsContext);
return idKey && data ? getAccountsById(data) : data;
}

View File

@@ -34,7 +34,7 @@ export function CachedPayees({ children, idKey }) {
return children(data);
}
export function useCachedPayees({ idKey } = {}) {
export function useCachedPayees({ idKey }: { idKey? } = {}) {
let data = useContext(PayeesContext);
return idKey && data ? getPayeesById(data) : data;
}

View File

@@ -21,7 +21,8 @@ function loadStatuses(schedules, onData) {
});
}
export function useSchedules({ transform } = {}) {
type UseSchedulesArgs = { transform?: <T>(v: T) => T };
export function useSchedules({ transform }: UseSchedulesArgs = {}) {
let [data, setData] = useState(null);
useEffect(() => {

View File

@@ -2,6 +2,8 @@ import { captureException, captureBreadcrumb } from '../../exceptions';
import * as uuid from '../../uuid';
import * as undo from '../undo';
import type * as T from '.';
let replyHandlers = new Map();
let listeners = new Map();
let messageQueue = [];
@@ -133,13 +135,17 @@ function connectWorker(worker, onOpen, onError) {
}
}
export const init = async function (worker) {
export const init: T.Init = async function (worker) {
return new Promise((resolve, reject) =>
connectWorker(worker, resolve, reject),
);
};
export const send = function (name, args, { catchErrors = false } = {}) {
export const send: T.Send = function (
name,
args,
{ catchErrors = false } = {},
) {
return new Promise((resolve, reject) => {
uuid.v4().then(id => {
replyHandlers.set(id, { resolve, reject });
@@ -156,14 +162,15 @@ export const send = function (name, args, { catchErrors = false } = {}) {
globalWorker.postMessage(message);
}
});
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any;
};
export const sendCatch = function (name, args) {
export const sendCatch: T.SendCatch = function (name, args) {
return send(name, args, { catchErrors: true });
};
export const listen = function (name, cb) {
export const listen: T.Listen = function (name, cb) {
if (!listeners.get(name)) {
listeners.set(name, []);
}
@@ -178,6 +185,6 @@ export const listen = function (name, cb) {
};
};
export const unlisten = function (name) {
export const unlisten: T.Unlisten = function (name) {
listeners.set(name, []);
};

View File

@@ -0,0 +1,23 @@
import type { Handlers } from '../../../types/handlers';
export function init(socketName: string): Promise<unknown>;
export type Init = typeof init;
export function send<K extends keyof Handlers>(
name: K,
args?: Parameters<Handlers[K]>[0],
options?: { catchErrors?: boolean },
): ReturnType<Handlers[K]>;
export type Send = typeof send;
export function sendCatch<K extends keyof Handlers>(
name: K,
args?: Parameters<Handlers[K]>[0],
): ReturnType<Handlers[K]>;
export type SendCatch = typeof sendCatch;
export function listen(name: string, cb: () => void): () => void;
export type Listen = typeof listen;
export function unlisten(name: string): void;
export type Unlisten = typeof unlisten;

View File

@@ -1,6 +1,8 @@
import * as uuid from '../../uuid';
import * as undo from '../undo';
import type * as T from '.';
let replyHandlers = new Map();
let listeners = new Map();
let messageQueue = [];
@@ -75,11 +77,15 @@ function connectSocket(name, onOpen) {
});
}
export const init = async function (socketName) {
export const init: T.Init = async function (socketName) {
return new Promise(resolve => connectSocket(socketName, resolve));
};
export const send = function (name, args, { catchErrors = false } = {}) {
export const send: T.Send = function (
name,
args,
{ catchErrors = false } = {},
) {
return new Promise((resolve, reject) => {
uuid.v4().then(id => {
replyHandlers.set(id, { resolve, reject });
@@ -102,14 +108,15 @@ export const send = function (name, args, { catchErrors = false } = {}) {
});
}
});
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any;
};
export const sendCatch = function (name, args) {
export const sendCatch: T.SendCatch = function (name, args) {
return send(name, args, { catchErrors: true });
};
export const listen = function (name, cb) {
export const listen: T.Listen = function (name, cb) {
if (!listeners.get(name)) {
listeners.set(name, []);
}
@@ -126,6 +133,6 @@ export const listen = function (name, cb) {
};
};
export const unlisten = function (name) {
export const unlisten: T.Unlisten = function (name) {
listeners.set(name, []);
};

View File

@@ -0,0 +1,20 @@
export function init(): void;
export type Init = typeof init;
export function getItem(key: string): Promise<string>;
export type GetItem = typeof getItem;
export function setItem(key: string, value: unknown): void;
export type SetItem = typeof setItem;
export function removeItem(key: string): void;
export type RemoveItem = typeof removeItem;
export function multiGet(keys: string[]): Promise<[string, unknown][]>;
export type MultiGet = typeof multiGet;
export function multiSet(keyValues: [string, unknown][]): void;
export type MultiSet = typeof multiSet;
export function multiRemove(keys: string[]): void;
export type MultiRemove = typeof multiRemove;

View File

@@ -3,11 +3,13 @@ import { join } from 'path';
import * as lootFs from '../fs';
import * as T from '.';
let getStorePath = () => join(lootFs.getDataDir(), 'global-store.json');
let store;
let persisted = true;
export const init = function ({ persist = true } = {}) {
export const init: T.Init = function ({ persist = true } = {}) {
if (persist) {
try {
store = JSON.parse(fs.readFileSync(getStorePath(), 'utf8'));
@@ -36,23 +38,23 @@ function _saveStore() {
}
}
export const getItem = function (key) {
export const getItem: T.GetItem = function (key) {
return new Promise(function (resolve) {
return resolve(store[key]);
});
};
export const setItem = function (key, value) {
export const setItem: T.SetItem = function (key, value) {
store[key] = value;
return _saveStore();
};
export const removeItem = function (key) {
export const removeItem: T.RemoveItem = function (key) {
delete store[key];
return _saveStore();
};
export const multiGet = function (keys) {
export const multiGet: T.MultiGet = function (keys) {
return new Promise(function (resolve) {
return resolve(
keys.map(function (key) {
@@ -62,14 +64,14 @@ export const multiGet = function (keys) {
});
};
export const multiSet = function (keyValues) {
export const multiSet: T.MultiSet = function (keyValues) {
keyValues.forEach(function ([key, value]) {
store[key] = value;
});
return _saveStore();
};
export const multiRemove = function (keys) {
export const multiRemove: T.MultiRemove = function (keys) {
keys.forEach(function (key) {
delete store[key];
});

View File

@@ -1,22 +1,24 @@
import * as T from '.';
const store = {};
export const init = function () {};
export const init: T.Init = function () {};
export const getItem = function (key) {
export const getItem: T.GetItem = function (key) {
return new Promise(function (resolve) {
return resolve(store[key]);
});
};
export const setItem = function (key, value) {
export const setItem: T.SetItem = function (key, value) {
store[key] = value;
};
export const removeItem = function (key) {
export const removeItem: T.RemoveItem = function (key) {
delete store[key];
};
export const multiGet = function (keys) {
export const multiGet: T.MultiGet = function (keys) {
return new Promise(function (resolve) {
return resolve(
keys.map(function (key) {
@@ -26,13 +28,13 @@ export const multiGet = function (keys) {
});
};
export const multiSet = function (keyValues) {
export const multiSet: T.MultiSet = function (keyValues) {
keyValues.forEach(function ([key, value]) {
store[key] = value;
});
};
export const multiRemove = function (keys) {
export const multiRemove: T.MultiRemove = function (keys) {
keys.forEach(function (key) {
delete store[key];
});

View File

@@ -1,6 +1,8 @@
import { getDatabase } from '../indexeddb';
export const init = function () {};
import * as T from '.';
export const init: T.Init = function () {};
function commit(trans) {
if (trans.commit) {
@@ -8,7 +10,7 @@ function commit(trans) {
}
}
export const getItem = async function (key) {
export const getItem: T.GetItem = async function (key) {
let db = await getDatabase();
let transaction = db.transaction(['asyncStorage'], 'readonly');
@@ -22,7 +24,7 @@ export const getItem = async function (key) {
});
};
export const setItem = async function (key, value) {
export const setItem: T.SetItem = async function (key, value) {
let db = await getDatabase();
let transaction = db.transaction(['asyncStorage'], 'readwrite');
@@ -36,7 +38,7 @@ export const setItem = async function (key, value) {
});
};
export const removeItem = async function (key) {
export const removeItem: T.RemoveItem = async function (key) {
let db = await getDatabase();
let transaction = db.transaction(['asyncStorage'], 'readwrite');
@@ -50,7 +52,7 @@ export const removeItem = async function (key) {
});
};
export const multiGet = async function (keys) {
export const multiGet: T.MultiGet = async function (keys) {
let db = await getDatabase();
let transaction = db.transaction(['asyncStorage'], 'readonly');
@@ -58,7 +60,7 @@ export const multiGet = async function (keys) {
let promise = Promise.all(
keys.map(key => {
return new Promise((resolve, reject) => {
return new Promise<[string, unknown]>((resolve, reject) => {
let req = objectStore.get(key);
req.onerror = e => reject(e);
req.onsuccess = e => resolve([key, e.target.result]);
@@ -70,7 +72,7 @@ export const multiGet = async function (keys) {
return promise;
};
export const multiSet = async function (keyValues) {
export const multiSet: T.MultiSet = async function (keyValues) {
let db = await getDatabase();
let transaction = db.transaction(['asyncStorage'], 'readwrite');
@@ -90,7 +92,7 @@ export const multiSet = async function (keyValues) {
return promise;
};
export const multiRemove = async function (keys) {
export const multiRemove: T.MultiRemove = async function (keys) {
let db = await getDatabase();
let transaction = db.transaction(['asyncStorage'], 'readwrite');

View File

@@ -1,4 +1,7 @@
export function fetch(input: RequestInfo | URL): Promise<unknown>;
export function fetch(
input: RequestInfo | URL,
options?: unknown,
): Promise<Response>;
export function fetchBinary(
input: RequestInfo | URL,
filepath: string,

View File

@@ -1,6 +1,6 @@
import type Logger from '.';
import type * as T from '.';
const logger: Logger = {
const logger: T.Logger = {
info: (...args) => {
console.log(...args);
},

View File

@@ -1,9 +1,10 @@
import { type Transports } from 'electron-log';
interface Logger {
export interface Logger {
info(...args: unknown[]): void;
warn(...args: unknown[]): void;
transports?: Transports;
}
export default Logger;
const logger: Logger;
export default logger;

View File

@@ -1,6 +1,6 @@
import logger from 'electron-log';
import type Logger from '.';
import type * as T from '.';
if (logger.transports) {
logger.transports.file.appName = 'Actual';
@@ -9,4 +9,4 @@ if (logger.transports) {
logger.transports.console.level = false;
}
export default logger as Logger;
export default logger as T.Logger;

View File

@@ -1,6 +1,6 @@
import type Logger from '.';
import type * as T from '.';
const logger: Logger = {
const logger: T.Logger = {
info: (...args) => {
console.log(...args);
},

View File

@@ -1,3 +1,5 @@
import { type Database } from 'better-sqlite3';
export async function init(): unknown;
export function _getModule(): SqlJsStatic;
@@ -7,15 +9,15 @@ export function prepare(db, sql): unknown;
export function runQuery(
db: unknown,
sql: string,
params?: string[],
params?: (string | number)[],
fetchAll?: false,
): { changes: unknown };
export function runQuery(
export function runQuery<T>(
db: unknown,
sql: string,
params: string[],
params: (string | number)[],
fetchAll: true,
): unknown[];
): T[];
export function execQuery(db, sql): void;
@@ -23,7 +25,7 @@ export function transaction(db, fn): unknown;
export async function asyncTransaction(db, fn): unknown;
export async function openDatabase(pathOrBuffer?: string | Buffer): unknown;
export async function openDatabase(pathOrBuffer?: string | Buffer): Database;
export function closeDatabase(db): void;

View File

@@ -87,10 +87,10 @@ export function openDatabase(pathOrBuffer) {
let db = new Database(pathOrBuffer);
// Define Unicode-aware LOWER and UPPER implementation.
// This is necessary because better-sqlite3 uses SQLite build without ICU support.
db.function('UNICODE_LOWER', { deterministic: true }, arg =>
db.function('UNICODE_LOWER', { deterministic: true }, (arg: string | null) =>
arg?.toLowerCase(),
);
db.function('UNICODE_UPPER', { deterministic: true }, arg =>
db.function('UNICODE_UPPER', { deterministic: true }, (arg: string | null) =>
arg?.toUpperCase(),
);
return db;

View File

@@ -28,7 +28,11 @@ async function getTransactions(accountId) {
);
}
async function importFileWithRealTime(accountId, filepath, dateFormat) {
async function importFileWithRealTime(
accountId,
filepath,
dateFormat?: string,
) {
// Emscripten requires a real Date.now!
global.restoreDateNow();
let { errors, transactions } = await parseFile(filepath);

View File

@@ -7,7 +7,7 @@ import { looselyParseAmount } from '../../shared/util';
import qif2json from './qif2json';
export function parseFile(filepath, options) {
export function parseFile(filepath, options?: unknown) {
let errors = [];
let m = filepath.match(/\.[^.]*$/);
@@ -30,10 +30,10 @@ export function parseFile(filepath, options) {
message: 'Invalid file type',
internal: '',
});
return { errors };
return { errors, transactions: undefined };
}
async function parseCSV(filepath, options = {}) {
async function parseCSV(filepath, options: { delimiter?: string } = {}) {
let errors = [];
let contents = await fs.readFile(filepath);
@@ -148,6 +148,7 @@ async function parseOFX(filepath) {
ofxParser: true,
which: 'both',
errors: {
length: oldErrors.length + newErrors.length,
oldErrors,
newErrors,
},
@@ -157,6 +158,7 @@ async function parseOFX(filepath) {
ofxParser: true,
which: 'old',
errors: {
length: oldErrors.length,
oldErrors,
},
transactions: newTrans,
@@ -166,6 +168,7 @@ async function parseOFX(filepath) {
ofxParser: true,
which: 'new',
errors: {
length: newErrors.length,
newErrors,
},
transactions: oldTrans,
@@ -196,7 +199,7 @@ async function parseOfxJavascript(filepath) {
// not sure about browser. We want latin1 and not utf8.
// For some reason, utf8 does not parse ofx files correctly here.
const contents = new TextDecoder('latin1').decode(
await fs.readFile(filepath, 'binary'),
(await fs.readFile(filepath, 'binary')) as Buffer,
);
let data;

View File

@@ -1,19 +1,44 @@
export default function parse(qif, options) {
var lines = qif.split('\n'),
line = lines.shift(),
type = /!Type:([^$]*)$/.exec(line.trim()),
data = {},
transactions = (data.transactions = []),
transaction = {};
type Division = {
category?: string;
subcategory?: string;
description?: string;
amount?: number;
};
options = options || {};
type QIFTransaction = {
date?: string;
amount?: string;
number?: string;
memo?: string;
address?: string[];
clearedStatus?: string;
category?: string;
subcategory?: string;
payee?: string;
division?: Division[];
};
export default function parse(qif, options: { dateFormat?: string } = {}) {
let lines = qif.split('\n');
let line = lines.shift();
let type = /!Type:([^$]*)$/.exec(line.trim());
let data: {
dateFormat: string | undefined;
type?;
transactions: QIFTransaction[];
} = {
dateFormat: options.dateFormat,
transactions: [],
};
let transactions = data.transactions;
let transaction: QIFTransaction = {};
if (!type || !type.length) {
throw new Error('File does not appear to be a valid qif file: ' + line);
}
data.type = type[1];
var division = {};
let division: Division = {};
while ((line = lines.shift())) {
line = line.trim();
@@ -44,7 +69,7 @@ export default function parse(qif, options) {
transaction.payee = line.substring(1).replace(/&amp;/g, '&');
break;
case 'L':
var lArray = line.substring(1).split(':');
let lArray = line.substring(1).split(':');
transaction.category = lArray[0];
if (lArray[1] !== undefined) {
transaction.subcategory = lArray[1];
@@ -54,7 +79,7 @@ export default function parse(qif, options) {
transaction.clearedStatus = line.substring(1);
break;
case 'S':
var sArray = line.substring(1).split(':');
let sArray = line.substring(1).split(':');
division.category = sArray[0];
if (sArray[1] !== undefined) {
division.subcategory = sArray[1];
@@ -81,7 +106,5 @@ export default function parse(qif, options) {
if (Object.keys(transaction).length) {
transactions.push(transaction);
}
data.dateFormat = options.dateFormat;
return data;
}

View File

@@ -199,6 +199,14 @@ let CONDITION_TYPES = {
};
export class Condition {
field;
op;
options;
rawValue;
type;
unparsedValue;
value;
constructor(op, field, value, options, fieldTypes) {
let typeName = fieldTypes.get(field);
assert(typeName, 'internal', 'Invalid condition field: ' + field);
@@ -393,6 +401,13 @@ export class Condition {
let ACTION_OPS = ['set', 'link-schedule'];
export class Action {
field;
op;
options;
rawValue;
type;
value;
constructor(op, field, value, options, fieldTypes) {
assert(
ACTION_OPS.includes(op),
@@ -440,7 +455,27 @@ export class Action {
}
export class Rule {
constructor({ id, stage, conditionsOp, conditions, actions, fieldTypes }) {
actions;
conditions;
conditionsOp;
id;
stage;
constructor({
id,
stage,
conditionsOp,
conditions,
actions,
fieldTypes,
}: {
id?: string;
stage?;
conditionsOp;
conditions;
actions;
fieldTypes;
}) {
this.id = id;
this.stage = stage;
this.conditionsOp = conditionsOp;
@@ -498,7 +533,11 @@ export class Rule {
}
export class RuleIndexer {
constructor({ field, method }) {
field;
method;
rules;
constructor({ field, method }: { field: string; method?: string }) {
this.field = field;
this.method = method;
this.rules = new Map();

View File

@@ -19,8 +19,17 @@ import * as transfer from './transfer';
const papaJohns = 'Papa Johns east side';
const lowes = 'Lowes Store';
jest.mock('../../shared/months', () => ({
...jest.requireActual('../../shared/months'),
currentDay: jest.fn(),
currentMonth: jest.fn(),
}));
beforeEach(async () => {
mockSyncServer.reset();
jest.resetAllMocks();
(monthUtils.currentDay as jest.Mock).mockReturnValue('2017-10-15');
(monthUtils.currentMonth as jest.Mock).mockReturnValue('2017-10');
await global.emptyDatabase()();
await loadMappings();
await loadRules();
@@ -103,7 +112,6 @@ async function getAllPayees() {
describe('Account sync', () => {
test('reconcile creates payees correctly', async () => {
monthUtils.currentDay = () => '2017-10-15';
let { id } = await prepareDatabase();
let payees = await getAllPayees();
@@ -128,7 +136,6 @@ describe('Account sync', () => {
});
test('reconcile matches single transaction', async () => {
monthUtils.currentDay = () => '2017-10-15';
let mockTransactions = prepMockTransactions();
const { id, account_id } = await prepareDatabase();
@@ -166,7 +173,6 @@ describe('Account sync', () => {
});
test('reconcile matches multiple transactions', async () => {
monthUtils.currentDay = () => '2017-10-15';
let mockTransactions = prepMockTransactions();
const { id, account_id } = await prepareDatabase();
@@ -231,7 +237,6 @@ describe('Account sync', () => {
});
test('reconcile matches multiple transactions (imported_id wins)', async () => {
monthUtils.currentDay = () => '2017-10-15';
let mockTransactions = prepMockTransactions();
const { id, account_id } = await prepareDatabase();
@@ -277,7 +282,6 @@ describe('Account sync', () => {
});
test('import never matches existing with financial ids', async () => {
monthUtils.currentDay = () => '2017-10-15';
let mockTransactions = prepMockTransactions();
const { id, account_id } = await prepareDatabase();
@@ -312,14 +316,13 @@ describe('Account sync', () => {
differ.expectToMatchDiff(await getAllTransactions());
monthUtils.currentDay = () => '2017-10-17';
(monthUtils.currentDay as jest.Mock).mockReturnValue('2017-10-17');
await syncAccount('userId', 'userKey', id, account_id, 'bank');
differ.expectToMatchDiff(await getAllTransactions());
});
test('import updates transfers when matched', async () => {
monthUtils.currentDay = () => '2017-10-15';
let mockTransactions = prepMockTransactions();
const { id, account_id } = await prepareDatabase();
await db.insertAccount({ id: 'two', name: 'two' });
@@ -347,7 +350,7 @@ describe('Account sync', () => {
differ.expectToMatchDiff(await getAllTransactions());
monthUtils.currentDay = () => '2017-10-17';
(monthUtils.currentDay as jest.Mock).mockReturnValue('2017-10-17');
await syncAccount('userId', 'userKey', id, account_id, 'bank');
// Don't use `differ.expectToMatchDiff` because there's too many
@@ -607,16 +610,13 @@ describe('Account sync', () => {
});
test('imports transactions for current day and adds latest', async () => {
monthUtils.currentDay = () => '2017-10-15';
monthUtils.currentMonth = () => '2017-10';
const { id, account_id } = await prepareDatabase();
expect((await getAllTransactions()).length).toBe(0);
await syncAccount('userId', 'userKey', id, account_id, 'bank');
expect(await getAllTransactions()).toMatchSnapshot();
monthUtils.currentDay = () => '2017-10-17';
(monthUtils.currentDay as jest.Mock).mockReturnValue('2017-10-17');
await syncAccount('userId', 'userKey', id, account_id, 'bank');
expect(await getAllTransactions()).toMatchSnapshot();

View File

@@ -116,7 +116,7 @@ async function downloadTransactions(
acctId,
bankId,
since,
count,
count?: number,
) {
let allTransactions = [];
let accountBalance = null;
@@ -180,7 +180,6 @@ async function downloadNordigenTransactions(
acctId,
bankId,
since,
count,
) {
let userToken = await asyncStorage.getItem('user-token');
if (userToken) {
@@ -213,7 +212,7 @@ async function downloadNordigenTransactions(
return {
transactions: booked,
accountBalances: balances,
accountBalance: balances,
startingBalance,
};
}
@@ -243,7 +242,7 @@ async function resolvePayee(trans, payeeName, payeesToCreate) {
async function normalizeTransactions(
transactions,
acctId,
{ rawPayeeName } = {},
{ rawPayeeName = false } = {},
) {
let payeesToCreate = new Map();

View File

@@ -78,7 +78,7 @@ function toInternalField(obj) {
}
export const ruleModel = {
validate(rule, { update } = {}) {
validate(rule, { update }: { update?: boolean } = {}) {
requiredFields('rules', rule, ['conditions', 'actions'], update);
if (!update || 'stage' in rule) {
@@ -513,7 +513,7 @@ function* getIsSetterRules(
stage,
condField,
actionField,
{ condValue, actionValue },
{ condValue, actionValue }: { condValue?: string; actionValue?: string },
) {
let rules = getRules();
for (let i = 0; i < rules.length; i++) {
@@ -541,7 +541,7 @@ function* getOneOfSetterRules(
stage,
condField,
actionField,
{ condValue, actionValue },
{ condValue, actionValue }: { condValue?: string; actionValue: string },
) {
let rules = getRules();
for (let i = 0; i < rules.length; i++) {

View File

@@ -6,7 +6,7 @@ import { batchMessages } from '../sync';
import * as rules from './transaction-rules';
import * as transfer from './transfer';
async function idsWithChildren(ids) {
async function idsWithChildren(ids: string[]) {
let whereIds = whereIn(ids, 'parent_id');
let rows = await db.all(
`SELECT id FROM v_transactions_internal WHERE ${whereIds}`,
@@ -18,7 +18,7 @@ async function idsWithChildren(ids) {
return [...set];
}
async function getTransactionsByIds(ids) {
async function getTransactionsByIds(ids: string[]) {
// TODO: convert to whereIn
//
// or better yet, use ActualQL
@@ -37,6 +37,17 @@ export async function batchUpdateTransactions({
updated,
learnCategories = false,
detectOrphanPayees = true,
}: {
added?: Array<{ id: string; payee: unknown; category: unknown }>;
deleted?: Array<{ id: string; payee: unknown }>;
updated?: Array<{
id: string;
payee: unknown;
account: unknown;
category: unknown;
}>;
learnCategories?: boolean;
detectOrphanPayees?: boolean;
}) {
// Track the ids of each type of transaction change (see below for why)
let addedIds = [];

View File

@@ -34,11 +34,22 @@ async function prepareDatabase() {
});
}
type Transaction = {
account: string;
amount: number;
category?: string;
date: string;
id?: string;
notes?: string;
payee: string;
transfer_id?: string;
};
describe('Transfer', () => {
test('transfers are properly inserted/updated/deleted', async () => {
await prepareDatabase();
let transaction = {
let transaction: Transaction = {
account: 'one',
amount: 5000,
payee: await db.insertPayee({ name: 'Non-transfer' }),
@@ -126,7 +137,7 @@ describe('Transfer', () => {
"SELECT * FROM payees WHERE transfer_acct = 'three'",
);
let transaction = {
let transaction: Transaction = {
account: 'one',
amount: 5000,
payee: await db.insertPayee({ name: 'Non-transfer' }),

View File

@@ -324,7 +324,7 @@ function castInput(state, expr, type) {
}
// TODO: remove state from these functions
function val(state, expr, type) {
function val(state, expr, type?: string) {
let castedExpr = expr;
// Cast the type if necessary
@@ -347,11 +347,11 @@ function val(state, expr, type) {
return castedExpr.value;
}
function valArray(state, arr, types) {
function valArray(state, arr: unknown[], types?: string[]) {
return arr.map((value, idx) => val(state, value, types ? types[idx] : null));
}
function validateArgLength(arr, min, max) {
function validateArgLength(arr: unknown[], min: number, max?: number) {
if (max == null) {
max = min;
}
@@ -931,7 +931,7 @@ function isAggregateFunction(expr) {
return true;
}
return !!argExprs.find(ex => isAggregateFunction(ex));
return !!(argExprs as unknown[]).find(ex => isAggregateFunction(ex));
}
export function isAggregateQuery(queryState) {
@@ -952,7 +952,16 @@ export function isAggregateQuery(queryState) {
});
}
export function compileQuery(queryState, schema, schemaConfig = {}) {
type SchemaConfig = {
tableViews?: Record<string, unknown> | ((...args: unknown[]) => unknown);
tableFilters?: (name: string) => unknown[];
customizeQuery?: <T>(queryString: T) => T;
};
export function compileQuery(
queryState,
schema,
schemaConfig: SchemaConfig = {},
) {
let { withDead, validateRefs = true, tableOptions, rawMode } = queryState;
let {
@@ -979,7 +988,7 @@ export function compileQuery(queryState, schema, schemaConfig = {}) {
return filters;
};
let tableRef = (name, isJoin) => {
let tableRef = (name: string, isJoin?: boolean) => {
let view =
typeof tableViews === 'function'
? tableViews(name, { withDead, isJoin, tableOptions })
@@ -1101,7 +1110,11 @@ export function defaultConstructQuery(queryState, state, sqlPieces) {
`;
}
export function generateSQLWithState(queryState, schema, schemaConfig) {
export function generateSQLWithState(
queryState,
schema?: unknown,
schemaConfig?: unknown,
) {
let { sqlPieces, state } = compileQuery(queryState, schema, schemaConfig);
return { sql: defaultConstructQuery(queryState, state, sqlPieces), state };
}

View File

@@ -16,7 +16,7 @@ function repeat(arr, times) {
return result;
}
function runQuery(query, options) {
function runQuery(query, options?: unknown) {
return aql.runQuery(schema, schemaConfig, query, options);
}

View File

@@ -84,7 +84,13 @@ export function convertOutputType(value, type) {
return value;
}
export function conform(schema, schemaConfig, table, obj, { skipNull } = {}) {
export function conform(
schema,
schemaConfig,
table,
obj,
{ skipNull = false } = {},
) {
let tableSchema = schema[table];
if (tableSchema == null) {
throw new Error(`Table “${table}” does not exist`);

View File

@@ -8,7 +8,7 @@ import { schemaExecutors } from './executors';
import { schema, schemaConfig } from './index';
export function runCompiledQuery(query, sqlPieces, state, params) {
export function runCompiledQuery(query, sqlPieces, state, params?: unknown) {
return _runCompiledQuery(query, sqlPieces, state, {
params,
executors: schemaExecutors,

View File

@@ -95,7 +95,7 @@ function handleAccountChange(months, oldValue, newValue) {
);
months.forEach(month => {
let sheetName = monthUtils.sheetForMonth(month, getBudgetType());
let sheetName = monthUtils.sheetForMonth(month);
rows.forEach(row => {
sheet
@@ -118,7 +118,7 @@ function handleTransactionChange(transaction, changedFields) {
transaction.category
) {
let month = monthUtils.monthFromDate(db.fromDateRepr(transaction.date));
let sheetName = monthUtils.sheetForMonth(month, getBudgetType());
let sheetName = monthUtils.sheetForMonth(month);
sheet
.get()
@@ -128,7 +128,7 @@ function handleTransactionChange(transaction, changedFields) {
function handleCategoryMappingChange(months, oldValue, newValue) {
months.forEach(month => {
let sheetName = monthUtils.sheetForMonth(month, getBudgetType());
let sheetName = monthUtils.sheetForMonth(month);
if (oldValue) {
sheet
.get()
@@ -197,8 +197,8 @@ function handleCategoryChange(months, oldValue, newValue) {
months.forEach(month => {
let prevMonth = monthUtils.prevMonth(month);
let prevSheetName = monthUtils.sheetForMonth(prevMonth, budgetType);
let sheetName = monthUtils.sheetForMonth(month, budgetType);
let prevSheetName = monthUtils.sheetForMonth(prevMonth);
let sheetName = monthUtils.sheetForMonth(month);
let { start, end } = monthUtils.bounds(month);
createCategory(newValue, sheetName, prevSheetName, start, end);
@@ -222,7 +222,7 @@ function handleCategoryChange(months, oldValue, newValue) {
let id = newValue.id;
months.forEach(month => {
let sheetName = monthUtils.sheetForMonth(month, budgetType);
let sheetName = monthUtils.sheetForMonth(month);
removeDeps(sheetName, oldValue.cat_group, id);
addDeps(sheetName, newValue.cat_group, id);
});
@@ -271,7 +271,7 @@ function handleCategoryGroupChange(months, oldValue, newValue) {
if (newValue.tombstone === 1 && oldValue && oldValue.tombstone === 0) {
let id = newValue.id;
months.forEach(month => {
let sheetName = monthUtils.sheetForMonth(month, budgetType);
let sheetName = monthUtils.sheetForMonth(month);
removeDeps(sheetName, id);
});
} else if (
@@ -282,7 +282,7 @@ function handleCategoryGroupChange(months, oldValue, newValue) {
if (!group.is_income || budgetType !== 'rollover') {
months.forEach(month => {
let sheetName = monthUtils.sheetForMonth(month, budgetType);
let sheetName = monthUtils.sheetForMonth(month);
// Dirty, dirty hack. These functions should not be async, but this is
// OK because we're leveraging the sync nature of queries. Ideally we
@@ -402,8 +402,8 @@ export async function createBudget(months) {
if (!meta.createdMonths.has(month)) {
let prevMonth = monthUtils.prevMonth(month);
let { start, end } = monthUtils.bounds(month);
let sheetName = monthUtils.sheetForMonth(month, budgetType);
let prevSheetName = monthUtils.sheetForMonth(prevMonth, budgetType);
let sheetName = monthUtils.sheetForMonth(month);
let prevSheetName = monthUtils.sheetForMonth(prevMonth);
categories.forEach(cat => {
createCategory(cat, sheetName, prevSheetName, start, end);

View File

@@ -7,7 +7,7 @@ import { number, sumAmounts, flatten2, unflatten2 } from './util';
function getBlankSheet(months) {
let blankMonth = monthUtils.prevMonth(months[0]);
return monthUtils.sheetForMonth(blankMonth, 'rollover');
return monthUtils.sheetForMonth(blankMonth);
}
export function createBlankCategory(cat, months) {

View File

@@ -0,0 +1,25 @@
export interface BudgetHandlers {
'budget/budget-amount': (...args: unknown[]) => Promise<unknown>;
'budget/copy-previous-month': (...args: unknown[]) => Promise<unknown>;
'budget/set-zero': (...args: unknown[]) => Promise<unknown>;
'budget/set-3month-avg': (...args: unknown[]) => Promise<unknown>;
'budget/apply-goal-template': (...args: unknown[]) => Promise<unknown>;
'budget/overwrite-goal-template': (...args: unknown[]) => Promise<unknown>;
'budget/hold-for-next-month': (...args: unknown[]) => Promise<unknown>;
'budget/reset-hold': (...args: unknown[]) => Promise<unknown>;
'budget/cover-overspending': (...args: unknown[]) => Promise<unknown>;
'budget/transfer-available': (...args: unknown[]) => Promise<unknown>;
'budget/transfer-category': (...args: unknown[]) => Promise<unknown>;
'budget/set-carryover': (...args: unknown[]) => Promise<unknown>;
}

View File

@@ -28,8 +28,8 @@ describe('merkle trie', () => {
});
test('diff returns the correct time difference', () => {
let trie1 = {};
let trie2 = {};
let trie1: { hash?: unknown } = {};
let trie2: { hash?: unknown } = {};
const messages = [
// First client messages
@@ -90,7 +90,7 @@ describe('merkle trie', () => {
message('2018-11-01T02:37:00.000Z-0000-0123456789ABCDEF', 2100),
];
let trie = {};
let trie: { hash?: unknown } = {};
messages.forEach(msg => {
trie = merkle.insert(trie, msg.timestamp);
});

View File

@@ -2,15 +2,16 @@ import { Timestamp } from './timestamp';
describe('Timestamp', function () {
let now = 0;
let prevNow;
beforeEach(function () {
Date.prevNow = Date.now;
prevNow = Date.now;
Date.now = () => now;
Timestamp.init({ node: '1' });
});
afterEach(() => {
Date.now = Date.prevNow;
Date.now = prevNow;
});
describe('comparison', function () {
@@ -24,7 +25,7 @@ describe('Timestamp', function () {
describe('parsing', function () {
it('should not parse', function () {
var invalidInputs = [
let invalidInputs = [
null,
undefined,
{},
@@ -40,19 +41,19 @@ describe('Timestamp', function () {
'9999-12-31T23:59:59.999Z-10000-FFFFFFFFFFFFFFFF',
'9999-12-31T23:59:59.999Z-FFFF-10000000000000000',
];
for (var invalidInput of invalidInputs) {
for (let invalidInput of invalidInputs) {
expect(Timestamp.parse(invalidInput)).toBe(null);
}
});
it('should parse', function () {
var validInputs = [
let validInputs = [
'1970-01-01T00:00:00.000Z-0000-0000000000000000',
'2015-04-24T22:23:42.123Z-1000-0123456789ABCDEF',
'9999-12-31T23:59:59.999Z-FFFF-FFFFFFFFFFFFFFFF',
];
for (var validInput of validInputs) {
var parsed = Timestamp.parse(validInput);
for (let validInput of validInputs) {
let parsed = Timestamp.parse(validInput);
expect(typeof parsed).toBe('object');
expect(parsed.millis() >= 0).toBeTruthy();
expect(parsed.millis() < 253402300800000).toBeTruthy();
@@ -117,7 +118,7 @@ describe('Timestamp', function () {
it('should fail with counter overflow', function () {
now = 40;
for (var i = 0; i < 65536; i++) Timestamp.send();
for (let i = 0; i < 65536; i++) Timestamp.send();
expect(Timestamp.send).toThrow(Timestamp.OverflowError);
});

View File

@@ -68,7 +68,7 @@ export function makeClientId() {
return uuid.v4Sync().replace(/-/g, '').slice(-16);
}
var config = {
let config = {
// Allow 5 minutes of clock drift
maxDrift: 5 * 60 * 1000,
};
@@ -80,7 +80,20 @@ const MAX_NODE_LENGTH = 16;
* timestamp instance class
*/
export class Timestamp {
constructor(millis, counter, node) {
static init;
static max;
static parse;
static recv;
static send;
static since;
static zero;
static ClockDriftError;
static DuplicateNodeError;
static OverflowError;
_state;
constructor(millis: number, counter: number, node: string) {
this._state = {
millis: millis,
counter: counter,
@@ -118,6 +131,8 @@ export class Timestamp {
}
class MutableTimestamp extends Timestamp {
static from;
setMillis(n) {
this._state.millis = n;
}
@@ -142,7 +157,7 @@ MutableTimestamp.from = timestamp => {
// Timestamp generator initialization
// * sets the node ID to an arbitrary value
// * useful for mocking/unit testing
Timestamp.init = function (options = {}) {
Timestamp.init = function (options: { maxDrift?: number; node?: string } = {}) {
if (options.maxDrift) {
config.maxDrift = options.maxDrift;
}
@@ -157,7 +172,6 @@ Timestamp.init = function (options = {}) {
: '',
),
),
null,
);
};
@@ -171,17 +185,17 @@ Timestamp.send = function () {
}
// retrieve the local wall time
var phys = Date.now();
let phys = Date.now();
// unpack the clock.timestamp logical time and counter
var lOld = clock.timestamp.millis();
var cOld = clock.timestamp.counter();
let lOld = clock.timestamp.millis();
let cOld = clock.timestamp.counter();
// calculate the next logical time and counter
// * ensure that the logical time never goes backward
// * increment the counter if phys time does not advance
var lNew = Math.max(lOld, phys);
var cNew = lOld === lNew ? cOld + 1 : 0;
let lNew = Math.max(lOld, phys);
let cNew = lOld === lNew ? cOld + 1 : 0;
// check the result for drift and counter overflow
if (lNew - phys > config.maxDrift) {
@@ -211,11 +225,11 @@ Timestamp.recv = function (msg) {
}
// retrieve the local wall time
var phys = Date.now();
let phys = Date.now();
// unpack the message wall time/counter
var lMsg = msg.millis();
var cMsg = msg.counter();
let lMsg = msg.millis();
let cMsg = msg.counter();
// assert the node id and remote clock drift
// if (msg.node() === clock.timestamp.node()) {
@@ -226,8 +240,8 @@ Timestamp.recv = function (msg) {
}
// unpack the clock.timestamp logical time and counter
var lOld = clock.timestamp.millis();
var cOld = clock.timestamp.counter();
let lOld = clock.timestamp.millis();
let cOld = clock.timestamp.counter();
// calculate the next logical time and counter
// . ensure that the logical time never goes backward
@@ -235,8 +249,8 @@ Timestamp.recv = function (msg) {
// . if max = old > message, increment local counter
// . if max = messsage > old, increment message counter
// . otherwise, clocks are monotonic, reset counter
var lNew = Math.max(Math.max(lOld, phys), lMsg);
var cNew =
let lNew = Math.max(Math.max(lOld, phys), lMsg);
let cNew =
lNew === lOld && lNew === lMsg
? Math.max(cOld, cMsg) + 1
: lNew === lOld
@@ -270,11 +284,11 @@ Timestamp.recv = function (msg) {
*/
Timestamp.parse = function (timestamp) {
if (typeof timestamp === 'string') {
var parts = timestamp.split('-');
let parts = timestamp.split('-');
if (parts && parts.length === 5) {
var millis = Date.parse(parts.slice(0, 3).join('-')).valueOf();
var counter = parseInt(parts[3], 16);
var node = parts[4];
let millis = Date.parse(parts.slice(0, 3).join('-')).valueOf();
let counter = parseInt(parts[3], 16);
let node = parts[4];
if (
!isNaN(millis) &&
millis >= 0 &&
@@ -293,7 +307,7 @@ Timestamp.parse = function (timestamp) {
/**
* zero/minimum timestamp
*/
var zero = Timestamp.parse('1970-01-01T00:00:00.000Z-0000-0000000000000000');
let zero = Timestamp.parse('1970-01-01T00:00:00.000Z-0000-0000000000000000');
Timestamp.zero = function () {
return zero;
};
@@ -301,7 +315,7 @@ Timestamp.zero = function () {
/**
* maximum timestamp
*/
var max = Timestamp.parse('9999-12-31T23:59:59.999Z-FFFF-FFFFFFFFFFFFFFFF');
let max = Timestamp.parse('9999-12-31T23:59:59.999Z-FFFF-FFFFFFFFFFFFFFFF');
Timestamp.max = function () {
return max;
};
@@ -315,24 +329,21 @@ Timestamp.since = isoString => {
*/
Timestamp.DuplicateNodeError = class extends Error {
constructor(node) {
super();
this.type = 'DuplicateNodeError';
this.message = 'duplicate node identifier ' + node;
super('duplicate node identifier ' + node);
this.name = 'DuplicateNodeError';
}
};
Timestamp.ClockDriftError = class extends Error {
constructor(...args) {
super();
this.type = 'ClockDriftError';
this.message = ['maximum clock drift exceeded'].concat(args).join(' ');
super(['maximum clock drift exceeded'].concat(args).join(' '));
this.name = 'ClockDriftError';
}
};
Timestamp.OverflowError = class extends Error {
constructor() {
super();
this.type = 'OverflowError';
this.message = 'timestamp counter overflow';
super('timestamp counter overflow');
this.name = 'OverflowError';
}
};

View File

@@ -143,18 +143,18 @@ export function asyncTransaction(fn) {
// This function is marked as async because `runQuery` is no longer
// async. We return a promise here until we've audited all the code to
// make sure nothing calls `.then` on this.
export async function all(sql, params?: string[]) {
export async function all(sql, params?: (string | number)[]) {
return runQuery(sql, params, true);
}
export async function first(sql, params?: string[]) {
export async function first(sql, params?: (string | number)[]) {
const arr = await runQuery(sql, params, true);
return arr.length === 0 ? null : arr[0];
}
// The underlying sql system is now sync, but we can't update `first` yet
// without auditing all uses of it
export function firstSync(sql, params?: string[]) {
export function firstSync(sql, params?: (string | number)[]) {
const arr = runQuery(sql, params, true);
return arr.length === 0 ? null : arr[0];
}
@@ -162,7 +162,7 @@ export function firstSync(sql, params?: string[]) {
// This function is marked as async because `runQuery` is no longer
// async. We return a promise here until we've audited all the code to
// make sure nothing calls `.then` on this.
export async function run(sql, params?: string[]) {
export async function run(sql, params?: (string | number)[]) {
return runQuery(sql, params);
}
@@ -328,7 +328,7 @@ export async function moveCategoryGroup(id, targetId) {
await update('category_groups', { id, sort_order });
}
export async function deleteCategoryGroup(group, transferId) {
export async function deleteCategoryGroup(group, transferId?: string) {
const categories = await all('SELECT * FROM categories WHERE cat_group = ?', [
group.id,
]);
@@ -387,7 +387,7 @@ export function updateCategory(category) {
return update('categories', category);
}
export async function moveCategory(id, groupId, targetId) {
export async function moveCategory(id, groupId, targetId?: string) {
if (!groupId) {
throw new Error('moveCategory: groupId is required');
}
@@ -404,7 +404,7 @@ export async function moveCategory(id, groupId, targetId) {
await update('categories', { id, sort_order, cat_group: groupId });
}
export async function deleteCategory(category, transferId) {
export async function deleteCategory(category, transferId?: string) {
if (transferId) {
// We need to update all the deleted categories that currently
// point to the one we're about to delete so they all are
@@ -634,7 +634,7 @@ export async function getTransactionsByDate(
throw new Error('`getTransactionsByDate` is deprecated');
}
export async function getTransactions(accountId, arg2) {
export async function getTransactions(accountId, arg2?: unknown) {
if (arg2 !== undefined) {
throw new Error(
'`getTransactions` was given a second argument, it now only takes a single argument `accountId`',

View File

@@ -13,7 +13,7 @@ function midpoint(items, to) {
}
}
export function shoveSortOrders(items, targetId) {
export function shoveSortOrders(items, targetId?: string) {
const to = items.findIndex(item => item.id === targetId);
const target = items[to];
const before = items[to - 1];

View File

@@ -25,7 +25,7 @@ export async function incrFetch(
return results;
}
export function whereIn(ids, field) {
export function whereIn(ids: string[], field: string) {
let ids2 = [...new Set(ids)];
// eslint-disable-next-line rulesdir/typography
let filter = `${field} IN (` + ids2.map(id => `'${id}'`).join(',') + ')';

View File

@@ -33,7 +33,7 @@ const argv = require('yargs').options({
},
}).argv;
function getDatabase(shouldReset) {
function getDatabase() {
return sqlite.openDatabase(argv.db);
}
@@ -47,7 +47,7 @@ function create(migrationName) {
async function list(db) {
const migrationsDir = getMigrationsDir();
const applied = await getAppliedMigrations(getDatabase(), migrationsDir);
const applied = await getAppliedMigrations(getDatabase());
const all = await getMigrationList(migrationsDir);
const pending = getPending(applied, all);

View File

@@ -36,7 +36,7 @@ export function getUpMigration(id, names) {
}
export async function getAppliedMigrations(db) {
const rows = await sqlite.runQuery(
const rows = await sqlite.runQuery<{ id: number }>(
db,
'SELECT * FROM __migrations__ ORDER BY id ASC',
[],

Some files were not shown because too many files have changed in this diff Show More