✏ README, fix start-once bug and refactor

- Added some configuration and running to README
- Refactored some code
- Fixed exception when stopping a start-once script
- Only allow running with pnpm
- Moved transactions into sparebank1Api.ts
- Formatted
This commit is contained in:
2025-01-25 22:30:52 +01:00
parent 4977e7ad6a
commit 2e73baf98b
9 changed files with 91 additions and 57 deletions

View File

@ -1,4 +1,3 @@
// TODO move types
import { BANK_INITIAL_REFRESH_TOKEN } from "@/../config.ts"
import logger from "@/logger.ts"
import dayjs from "dayjs"
@ -9,7 +8,6 @@ import {
type TokenResponse,
} from "@/bank/db/queries.ts"
import * as Api from "./sparebank1Api.ts"
import { toISODateString } from "@/date.ts"
export interface OAuthTokenResponse {
access_token: string
@ -39,12 +37,11 @@ export type Bank = Sparebank1
export interface Sparebank1 {
transactionsPastDay: (
accountKeys: ReadonlyArray<string> | string,
...accountKeys: ReadonlyArray<string>
) => Promise<TransactionResponse>
}
export class Sparebank1Impl implements Sparebank1 {
private static baseUrl = "https://api.sparebank1.no"
private readonly db: Database
constructor(db: Database) {
@ -80,7 +77,6 @@ export class Sparebank1Impl implements Sparebank1 {
async fetchNewTokens(): Promise<OAuthTokenResponse> {
const refreshToken = await this.getRefreshToken()
logger.debug(`Found refresh token '${refreshToken}'`)
const result = await Api.refreshToken(refreshToken)
if (result.status === "failure") {
@ -95,35 +91,13 @@ export class Sparebank1Impl implements Sparebank1 {
}
async transactionsPastDay(
accountKeys: ReadonlyArray<string> | string,
...accountKeys: ReadonlyArray<string>
): Promise<TransactionResponse> {
const today = dayjs()
const lastDay = today.subtract(1, "day")
const queries = new URLSearchParams({
// TODO allow multiple accountKeys
accountKey:
typeof accountKeys === "string" ? accountKeys : accountKeys[0],
fromDate: toISODateString(lastDay),
toDate: toISODateString(today),
return await Api.transactions(await this.getAccessToken(), accountKeys, {
fromDate: lastDay,
toDate: today,
})
const accessToken = await this.getAccessToken()
logger.debug(`Found access token '${accessToken}'`)
const url = `${Sparebank1Impl.baseUrl}/personal/banking/transactions?${queries}`
logger.debug(`Sending GET request to '${url}'`)
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/vnd.sparebank1.v1+json;charset=utf-8",
},
})
logger.debug(`Received response with status '${response.status}'`)
if (response.ok) {
return response.json()
} else {
logger.warn(await response.json())
return { transactions: [] }
}
}
}

View File

@ -1,6 +1,11 @@
import { BANK_OAUTH_CLIENT_ID, BANK_OAUTH_CLIENT_SECRET } from "../../config.ts"
import { OAuthTokenResponse } from "@/bank/sparebank1.ts"
import type {
OAuthTokenResponse,
TransactionResponse,
} from "@/bank/sparebank1.ts"
import logger from "@/logger.ts"
import { type Dayjs } from "dayjs"
import { toISODateString } from "@/date.ts"
const baseUrl = "https://api.sparebank1.no"
@ -16,6 +21,40 @@ function failure<T>(data: T): Failure<T> {
return { status: "failure", data: data }
}
export async function transactions(
accessToken: string,
accountKeys: string | ReadonlyArray<string>,
timePeriod?: {
fromDate: Dayjs
toDate: Dayjs
},
): Promise<TransactionResponse> {
const queries = new URLSearchParams({
// TODO allow multiple accountKeys
accountKey: typeof accountKeys === "string" ? accountKeys : accountKeys[0],
...(timePeriod && {
fromDate: toISODateString(timePeriod.fromDate),
toDate: toISODateString(timePeriod.toDate),
}),
})
const url = `${baseUrl}/personal/banking/transactions?${queries}`
logger.debug(`Sending GET request to '${url}'`)
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/vnd.sparebank1.v1+json;charset=utf-8",
},
})
logger.debug(`Received response with status '${response.status}'`)
if (response.ok) {
return response.json()
} else {
logger.warn(await response.json())
return { transactions: [] }
}
}
export async function refreshToken(
refreshToken: string,
): Promise<Result<OAuthTokenResponse, string>> {
@ -26,7 +65,7 @@ export async function refreshToken(
grant_type: "refresh_token",
})
const url = `${baseUrl}/oauth/token?${queries}`
logger.debug("Sending POST request to url: '%s'", url)
logger.debug(`Sending POST request to url: '${url}'`)
const response = await fetch(url, {
method: "post",
headers: {

View File

@ -16,6 +16,7 @@ import logger from "@/logger.ts"
import type { UUID } from "node:crypto"
import { createDb } from "@/bank/db/queries.ts"
import * as fs from "node:fs"
import { CronJob } from "cron"
// TODO Transports api for pino https://github.com/pinojs/pino/blob/HEAD/docs/transports.md
@ -27,12 +28,15 @@ export async function daily(actual: Actual, bank: Bank): Promise<void> {
// TODO multiple accounts
const accountId = ACTUAL_ACCOUNT_IDS[0] as UUID
const actualTransactions = transactions.map((transaction) =>
// TODO move to Bank interface?
bankTransactionIntoActualTransaction(transaction, accountId),
)
logger.debug(
`Mapped ${JSON.stringify(transactions)} to ${JSON.stringify(actualTransactions)} transactions`,
)
logger.trace({
aMessage: "Mapped from Bank to Actual",
from: JSON.stringify(transactions),
to: JSON.stringify(actualTransactions),
})
// TODO Import transactions into Actual
// If multiple accounts, loop over them
@ -48,7 +52,7 @@ export async function daily(actual: Actual, bank: Bank): Promise<void> {
async function fetchTransactionsFromPastDay(
bank: Bank,
): Promise<ReadonlyArray<Transaction>> {
const response = await bank.transactionsPastDay(BANK_ACCOUNT_IDS)
const response = await bank.transactionsPastDay(...BANK_ACCOUNT_IDS)
return response.transactions
}
@ -68,22 +72,24 @@ async function main(): Promise<void> {
const databaseFileName = `${DB_FILENAME}.sqlite`
const db = createDb(databaseFileName)
logger.info(`Started SQLlite database with filename="${databaseFileName}"`)
const bank = new Sparebank1Impl(db)
process.on("SIGINT", async () => {
logger.info("Caught interrupt signal")
await shutdown()
})
let cronJob: CronJob | undefined
if (process.env.ONCE) {
await daily(actual, new Sparebank1Impl(db))
await daily(actual, bank)
await shutdown()
return
}
logger.info("Waiting for CRON job to start")
const cronJob = cronJobDaily(async () => {
cronJob = cronJobDaily(async () => {
logger.info("Running daily job")
await daily(actual, new Sparebank1Impl(db))
await daily(actual, bank)
logger.info("Finished daily job")
})
@ -91,7 +97,7 @@ async function main(): Promise<void> {
logger.info("Shutting down")
await actual.shutdown()
db.close()
cronJob.stop()
cronJob?.stop()
}
}