From 3bf354b4bf642ff340d5ec0c859cf7ce61e0e6c0 Mon Sep 17 00:00:00 2001 From: Martin Berg Alstad <--get-all> Date: Thu, 26 Dec 2024 14:08:09 +0100 Subject: [PATCH] SQLite Moved DB queries to separate file Moved Sparebank1 API call to separate file Create database tokens table if it doesn't exist Signed-off-by: Martin Berg Alstad --- .gitignore | 4 ++++ default.sqlite | Bin 0 -> 4096 bytes src/bank/db/queries.ts | 19 +++++++++++++++++++ src/bank/sparebank1.ts | 28 ++++++++++++++-------------- src/bank/sparebank1Api.ts | 32 ++++++++++++++++++++++++++++++++ src/main.ts | 7 +++++-- 6 files changed, 74 insertions(+), 16 deletions(-) create mode 100644 src/bank/db/queries.ts create mode 100644 src/bank/sparebank1Api.ts diff --git a/.gitignore b/.gitignore index a708518..1655c91 100644 --- a/.gitignore +++ b/.gitignore @@ -174,3 +174,7 @@ dist # Finder (MacOS) folder config .DS_Store + +# SQLite +*.sqlite-shm +*.sqlite-wal diff --git a/default.sqlite b/default.sqlite index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e73d82588785fe665c982e8aa048cb6ed03d5dee 100644 GIT binary patch literal 4096 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|@t|AVoG{WYDXN;st3JAlr;ljiVtj n8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*O6ovo*_Dlxp literal 0 HcmV?d00001 diff --git a/src/bank/db/queries.ts b/src/bank/db/queries.ts new file mode 100644 index 0000000..e7b7c32 --- /dev/null +++ b/src/bank/db/queries.ts @@ -0,0 +1,19 @@ +import { type Database } from "better-sqlite3" +import { type OAuthTokenResponse } from "@/bank/sparebank1.ts" + +const tokenKey = "sparebank1" + +export function insertTokens( + db: Database, + oAuthToken: OAuthTokenResponse, +): void { + db.prepare("INSERT INTO tokens VALUES (?, ?)").run(tokenKey, oAuthToken) +} + +export function fetchTokens(db: Database): OAuthTokenResponse | null { + return ( + (db + .prepare("SELECT data FROM tokens WHERE key = ?") + .get(tokenKey) as OAuthTokenResponse) ?? null + ) +} diff --git a/src/bank/sparebank1.ts b/src/bank/sparebank1.ts index 186aac1..5dbe69a 100644 --- a/src/bank/sparebank1.ts +++ b/src/bank/sparebank1.ts @@ -1,11 +1,10 @@ // TODO move types -import { - BANK_INITIAL_REFRESH_TOKEN, - BANK_OAUTH_CLIENT_ID, - BANK_OAUTH_CLIENT_SECRET, -} from "@/../config.ts" +import { BANK_INITIAL_REFRESH_TOKEN } from "@/../config.ts" import logger from "@/logger.ts" import dayjs from "dayjs" +import { Database } from "better-sqlite3" +import { insertTokens } from "@/bank/db/queries.ts" +import * as Api from "./sparebank1Api.ts" export interface OAuthTokenResponse { access_token: string @@ -49,6 +48,11 @@ export class Sparebank1Impl implements Sparebank1 { private static baseUrl = "https://api.sparebank1.no" private _accessToken: AccessToken | undefined private _refreshToken: RefreshToken | undefined + private readonly db: Database + + constructor(db: Database) { + this.db = db + } private set accessToken(accessToken: AccessToken) { this._accessToken = accessToken @@ -83,19 +87,15 @@ export class Sparebank1Impl implements Sparebank1 { async fetchNewRefreshToken(): Promise { const refreshToken: string = await this.getRefreshToken() - const queries = new URLSearchParams({ - client_id: BANK_OAUTH_CLIENT_ID, - client_secret: BANK_OAUTH_CLIENT_SECRET, - refresh_token: refreshToken, - grant_type: "refresh_token", - }) - const response = await fetch(`${Sparebank1Impl.baseUrl}/token?${queries}`) + const result = await Api.refreshToken(refreshToken) - if (!response.ok) { + if (result.status === "failure") { throw new Error("Failed to fetch refresh token") } + const oAuthToken = result.data + + insertTokens(this.db, oAuthToken) - const oAuthToken: OAuthTokenResponse = await response.json() this.accessToken = { access_token: oAuthToken.access_token, expires_in: oAuthToken.expires_in, diff --git a/src/bank/sparebank1Api.ts b/src/bank/sparebank1Api.ts new file mode 100644 index 0000000..6e6855c --- /dev/null +++ b/src/bank/sparebank1Api.ts @@ -0,0 +1,32 @@ +import { BANK_OAUTH_CLIENT_ID, BANK_OAUTH_CLIENT_SECRET } from "../../config.ts" +import { OAuthTokenResponse } from "@/bank/sparebank1.ts" + +const baseUrl = "https://api.sparebank1.no" + +type Success = { status: "success"; data: T } +type Failure = { status: "failure"; data: T } +type Result = Success | Failure + +function success(data: T): Success { + return { status: "success", data: data } +} + +function failure(data: T): Failure { + return { status: "failure", data: data } +} + +export async function refreshToken( + refreshToken: string, +): Promise> { + const queries = new URLSearchParams({ + client_id: BANK_OAUTH_CLIENT_ID, + client_secret: BANK_OAUTH_CLIENT_SECRET, + refresh_token: refreshToken, + grant_type: "refresh_token", + }) + const response = await fetch(`${baseUrl}/token?${queries}`) + if (!response.ok) { + return failure(response.statusText) + } + return success(await response.json()) +} diff --git a/src/main.ts b/src/main.ts index 8f93b0e..42612e3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,7 +13,6 @@ import Database from "better-sqlite3" // TODO Transports api for pino https://github.com/pinojs/pino/blob/HEAD/docs/transports.md // TODO create .cache if missing -// TODO store oauth tokens in a SQLite db, use same dep as actual export async function daily(actual: Actual, bank: Bank): Promise { // Fetch transactions from the bank @@ -52,13 +51,17 @@ async function main(): Promise { const actual = await ActualImpl.init() const databaseFileName = "default.sqlite" const db = new Database(databaseFileName) + db.pragma("journal_mode = WAL") + db.exec( + "CREATE TABLE IF NOT EXISTS tokens (key VARCHAR PRIMARY KEY, data JSON)", + ) logger.info(`Started SQLlite database with filename="${databaseFileName}"`) logger.info("Waiting for CRON job to start") cronJobDaily(async () => { logger.info("Running daily job") - await daily(actual, new Sparebank1Impl()) + await daily(actual, new Sparebank1Impl(db)) logger.info("Finished daily job") })