mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-22 08:52:49 -05:00
@@ -1,4 +1,3 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env", "@babel/preset-typescript"],
|
||||
"ignore": ["**/__mocks__"]
|
||||
"presets": ["@babel/preset-env", "@babel/preset-typescript"]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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...' }));
|
||||
|
||||
@@ -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');
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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(() => {
|
||||
@@ -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, []);
|
||||
};
|
||||
|
||||
23
packages/loot-core/src/platform/client/fetch/index.d.ts
vendored
Normal file
23
packages/loot-core/src/platform/client/fetch/index.d.ts
vendored
Normal 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;
|
||||
@@ -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, []);
|
||||
};
|
||||
|
||||
20
packages/loot-core/src/platform/server/asyncStorage/index.d.ts
vendored
Normal file
20
packages/loot-core/src/platform/server/asyncStorage/index.d.ts
vendored
Normal 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;
|
||||
@@ -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];
|
||||
});
|
||||
|
||||
@@ -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];
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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(/&/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;
|
||||
}
|
||||
@@ -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();
|
||||
@@ -19,8 +19,17 @@ import * as transfer from './transfer';
|
||||
const papaJohns = 'Papa Johns east side';
|
||||
const lowes = 'Lowe’s 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();
|
||||
@@ -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();
|
||||
|
||||
@@ -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++) {
|
||||
@@ -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 = [];
|
||||
@@ -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' }),
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
@@ -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) {
|
||||
25
packages/loot-core/src/server/budget/types/handlers.d.ts
vendored
Normal file
25
packages/loot-core/src/server/budget/types/handlers.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
};
|
||||
@@ -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`',
|
||||
|
||||
@@ -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];
|
||||
@@ -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(',') + ')';
|
||||
@@ -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);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user