- Logging library
- Account Ids to .env
- Split logic in main function
This commit is contained in:
Martin Berg Alstad 2024-12-01 19:35:45 +01:00
parent 01af64349e
commit cc325b9f08
Signed by: martials
GPG Key ID: DF629A90917D1319
7 changed files with 146 additions and 39 deletions

View File

@ -7,3 +7,4 @@ SPAREBANK1_OAUTH_CLIENT_ID=your-client-id
SPAREBANK1_OAUTH_CLIENT_SECRET=your-client-secret
SPAREBANK1_OAUTH_STATE=your-state
SPAREBANK1_OAUTH_REDIRECT_URI=your-redirect-uri
BANK_ACCOUNT_IDS=your-account-id1,your-account-id2

View File

@ -8,6 +8,7 @@ export const ACTUAL_SYNC_ID = getOrThrow("ACTUAL_SYNC_ID")
export const ACTUAL_SERVER_URL = getOrThrow("ACTUAL_SERVER_URL")
export const ACTUAL_PASSWORD = getOrThrow("ACTUAL_PASSWORD")
export const ACTUAL_DATA_DIR = "data/cache"
export const ACTUAL_ACCOUNT_IDS = getArrayOrThrow("ACTUAL_ACCOUNT_IDS")
export const SPAREBANK1_OAUTH_CLIENT_ID = getOrThrow(
"SPAREBANK1_OAUTH_CLIENT_ID",
@ -19,9 +20,14 @@ export const SPAREBANK1_OAUTH_REDIRECT_URI = getOrThrow(
"SPAREBANK1_OAUTH_REDIRECT_URI",
)
export const SPAREBANK1_OAUTH_STATE = getOrThrow("SPAREBANK1_OAUTH_STATE")
export const BANK_ACCOUNT_IDS = getArrayOrThrow("BANK_ACCOUNT_IDS")
function getOrThrow(key: string): string {
const value = process.env[key]
assert(value, `Missing environment variable: ${key}`)
return value
}
function getArrayOrThrow(key: string): ReadonlyArray<string> {
return getOrThrow(key).split(",")
}

View File

@ -4,7 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
"start": "node --import=tsx ./src/main.ts",
"start": "node --import=tsx ./src/main.ts | pino-pretty",
"test": "echo \"Error: no test specified\" && exit 1",
"format": "prettier --write \"./**/*.{js,mjs,ts,md,json}\""
},
@ -15,6 +15,7 @@
"@actual-app/api": "^24.11.0",
"cron": "^3.2.1",
"dotenv": "^16.4.5",
"pino": "^9.5.0",
"prettier": "^3.3.3"
},
"devDependencies": {

93
pnpm-lock.yaml generated
View File

@ -17,6 +17,9 @@ importers:
dotenv:
specifier: ^16.4.5
version: 16.4.5
pino:
specifier: ^9.5.0
version: 9.5.0
prettier:
specifier: ^3.3.3
version: 3.3.3
@ -190,6 +193,10 @@ packages:
'@types/node@22.9.0':
resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==}
atomic-sleep@1.0.0:
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
engines: {node: '>=8.0.0'}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@ -246,6 +253,10 @@ packages:
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
engines: {node: '>=6'}
fast-redact@3.5.0:
resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==}
engines: {node: '>=6'}
fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
@ -315,9 +326,23 @@ packages:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
on-exit-leak-free@2.1.2:
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
engines: {node: '>=14.0.0'}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
pino-abstract-transport@2.0.0:
resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
pino-std-serializers@7.0.0:
resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==}
pino@9.5.0:
resolution: {integrity: sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==}
hasBin: true
prebuild-install@7.1.2:
resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==}
engines: {node: '>=10'}
@ -328,9 +353,15 @@ packages:
engines: {node: '>=14'}
hasBin: true
process-warning@4.0.0:
resolution: {integrity: sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==}
pump@3.0.2:
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
rc@1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
@ -339,12 +370,20 @@ packages:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
real-require@0.2.0:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safe-stable-stringify@2.5.0:
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
engines: {node: '>=10'}
semver@7.6.3:
resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
engines: {node: '>=10'}
@ -356,6 +395,13 @@ packages:
simple-get@4.0.1:
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
sonic-boom@4.2.0:
resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==}
split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
@ -370,6 +416,9 @@ packages:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'}
thread-stream@3.1.0:
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
tsx@4.19.2:
resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==}
engines: {node: '>=18.0.0'}
@ -494,6 +543,8 @@ snapshots:
dependencies:
undici-types: 6.19.8
atomic-sleep@1.0.0: {}
base64-js@1.5.1: {}
better-sqlite3@9.6.0:
@ -570,6 +621,8 @@ snapshots:
expand-template@2.0.3: {}
fast-redact@3.5.0: {}
fetch-blob@3.2.0:
dependencies:
node-domexception: 1.0.0
@ -624,10 +677,32 @@ snapshots:
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
on-exit-leak-free@2.1.2: {}
once@1.4.0:
dependencies:
wrappy: 1.0.2
pino-abstract-transport@2.0.0:
dependencies:
split2: 4.2.0
pino-std-serializers@7.0.0: {}
pino@9.5.0:
dependencies:
atomic-sleep: 1.0.0
fast-redact: 3.5.0
on-exit-leak-free: 2.1.2
pino-abstract-transport: 2.0.0
pino-std-serializers: 7.0.0
process-warning: 4.0.0
quick-format-unescaped: 4.0.4
real-require: 0.2.0
safe-stable-stringify: 2.5.0
sonic-boom: 4.2.0
thread-stream: 3.1.0
prebuild-install@7.1.2:
dependencies:
detect-libc: 2.0.3
@ -645,11 +720,15 @@ snapshots:
prettier@3.3.3: {}
process-warning@4.0.0: {}
pump@3.0.2:
dependencies:
end-of-stream: 1.4.4
once: 1.4.0
quick-format-unescaped@4.0.4: {}
rc@1.2.8:
dependencies:
deep-extend: 0.6.0
@ -663,10 +742,14 @@ snapshots:
string_decoder: 1.3.0
util-deprecate: 1.0.2
real-require@0.2.0: {}
resolve-pkg-maps@1.0.0: {}
safe-buffer@5.2.1: {}
safe-stable-stringify@2.5.0: {}
semver@7.6.3: {}
simple-concat@1.0.1: {}
@ -677,6 +760,12 @@ snapshots:
once: 1.4.0
simple-concat: 1.0.1
sonic-boom@4.2.0:
dependencies:
atomic-sleep: 1.0.0
split2@4.2.0: {}
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
@ -698,6 +787,10 @@ snapshots:
inherits: 2.0.4
readable-stream: 3.6.2
thread-stream@3.1.0:
dependencies:
real-require: 0.2.0
tsx@4.19.2:
dependencies:
esbuild: 0.23.1

View File

@ -1,32 +1,47 @@
import { type Actual, ActualImpl } from "@/actual.ts"
import { cronJobDaily } from "@/cron.ts"
import { type Bank, Sparebank1Impl } from "@/sparebank1.ts"
import { type Bank, Sparebank1Impl, type Transaction } from "@/sparebank1.ts"
import { transactionIntoActualTransaction } from "@/mappings.ts"
import { ACTUAL_ACCOUNT_IDS, BANK_ACCOUNT_IDS } from "../config.ts"
import logger from "pino"
import type { UUID } from "node:crypto"
async function daily(actual: Actual, bank: Bank): Promise<() => Promise<void>> {
return async () => {
console.log("Wake up! It's 1 AM!")
// TODO Transports api for pino https://github.com/pinojs/pino/blob/HEAD/docs/transports.md
async function daily(actual: Actual, bank: Bank): Promise<void> {
// Fetch transactions from the bank
const transactions = await bank.transactionsPastDay(
"my_account",
"my_access_token",
)
const transactions = await fetchTransactionsFromPastDay(bank)
logger().info(`Fetched ${transactions.length} transactions`)
// TODO account? id or name?
// TODO multiple accounts
const accountId = ACTUAL_ACCOUNT_IDS[0] as UUID
const actualTransactions = transactions.map((transaction) =>
transactionIntoActualTransaction(transaction, ""),
transactionIntoActualTransaction(transaction, accountId),
)
// TODO Import transactions into Actual
// If multiple accounts, loop over them
// Get account ID from mapper
await actual.importTransactions("a-b-c-d-e", actualTransactions)
await actual.importTransactions(accountId, actualTransactions)
}
async function fetchTransactionsFromPastDay(
bank: Bank,
): Promise<ReadonlyArray<Transaction>> {
// TODO refresh token
const { access_token } = await bank.refreshToken("my_refresh_token")
return bank.transactionsPastDay(BANK_ACCOUNT_IDS, access_token)
}
async function main(): Promise<void> {
logger().info("Starting application")
const actual = await ActualImpl.init()
cronJobDaily(await daily(actual, new Sparebank1Impl()))
cronJobDaily(async () => {
logger().info("Running daily job")
await daily(actual, new Sparebank1Impl())
logger().info("Finished daily job")
})
logger().info("Shutting down")
// await actual.shutdown()
}

View File

@ -1,14 +1,15 @@
import type { Transaction } from "@/sparebank1.ts"
import type { TransactionEntity } from "@actual-app/api/@types/loot-core/types/models"
import type { UUID } from "node:crypto"
// TODO more fields / correct fields?
export function transactionIntoActualTransaction(
transaction: Transaction,
account: string,
accountId: UUID,
): TransactionEntity {
return {
id: transaction.id,
account,
account: accountId,
amount: transaction.amount,
date: transaction.date,
payee: transaction.description,

View File

@ -1,9 +1,3 @@
import {
SPAREBANK1_OAUTH_CLIENT_ID,
SPAREBANK1_OAUTH_REDIRECT_URI,
SPAREBANK1_OAUTH_STATE,
} from "../config.ts"
// TODO move types
export interface OAuthTokenResponse {
access_token: string
@ -40,18 +34,14 @@ export interface Sparebank1 {
export class Sparebank1Impl implements Sparebank1 {
private baseUrl = "https://api.sparebank1.no"
// TODO remove?
async accessToken(): Promise<OAuthTokenResponse> {
const response = await fetch(`${this.baseUrl}/oauth/authorize?
client_id=${SPAREBANK1_OAUTH_CLIENT_ID}&
state=${SPAREBANK1_OAUTH_STATE}&
redirect_uri=${SPAREBANK1_OAUTH_REDIRECT_URI}&
finInst=fid-smn&
response_type=code`)
throw new Error("Not implemented")
if (response.ok) {
return await response.json()
}
throw new Error(`Failed to get access token. ${response.statusText}`)
// if (response.ok) {
// return await response.json()
// }
// throw new Error(`Failed to get access token. ${response.statusText}`)
}
async refreshToken(refreshToken: string): Promise<OAuthTokenResponse> {