Compare commits

...

2 Commits

Author SHA1 Message Date
Martin Berg Alstad
3bf354b4bf
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 <git@martials.no>
2024-12-26 14:08:09 +01:00
Martin Berg Alstad
8854a22b40
Added better-sqlite3 dep and created default db
Signed-off-by: Martin Berg Alstad <git@martials.no>
2024-12-26 12:49:54 +01:00
8 changed files with 103 additions and 16 deletions

4
.gitignore vendored
View File

@ -174,3 +174,7 @@ dist
# Finder (MacOS) folder config
.DS_Store
# SQLite
*.sqlite-shm
*.sqlite-wal

BIN
default.sqlite Normal file

Binary file not shown.

View File

@ -4,7 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
"start": "node --import=tsx ./src/main.ts | pino-pretty",
"start": "dotenvx run --env-file=.env.local -- node --import=tsx ./src/main.ts | pino-pretty",
"test": "dotenvx run --env-file=.env.test.local -- node --experimental-vm-modules node_modules/jest/bin/jest.js | pino-pretty",
"format": "prettier --write \"./**/*.{js,mjs,ts,md,json}\""
},
@ -14,6 +14,7 @@
"dependencies": {
"@actual-app/api": "^24.12.0",
"@dotenvx/dotenvx": "^1.31.3",
"better-sqlite3": "^11.7.0",
"cron": "^3.3.1",
"dayjs": "^1.11.13",
"dotenv": "^16.4.7",
@ -22,6 +23,7 @@
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@types/better-sqlite3": "^7.6.12",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.2",
"jest": "^29.7.0",

20
pnpm-lock.yaml generated
View File

@ -11,6 +11,9 @@ dependencies:
'@dotenvx/dotenvx':
specifier: ^1.31.3
version: 1.31.3
better-sqlite3:
specifier: ^11.7.0
version: 11.7.0
cron:
specifier: ^3.3.1
version: 3.3.1
@ -31,6 +34,9 @@ devDependencies:
'@jest/globals':
specifier: ^29.7.0
version: 29.7.0
'@types/better-sqlite3':
specifier: ^7.6.12
version: 7.6.12
'@types/jest':
specifier: ^29.5.14
version: 29.5.14
@ -993,6 +999,12 @@ packages:
'@babel/types': 7.26.3
dev: true
/@types/better-sqlite3@7.6.12:
resolution: {integrity: sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==}
dependencies:
'@types/node': 22.10.2
dev: true
/@types/graceful-fs@4.1.9:
resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
dependencies:
@ -1193,6 +1205,14 @@ packages:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
dev: false
/better-sqlite3@11.7.0:
resolution: {integrity: sha512-mXpa5jnIKKHeoGzBrUJrc65cXFKcILGZpU3FXR0pradUEm9MA7UZz02qfEejaMcm9iXrSOCenwwYMJ/tZ1y5Ig==}
requiresBuild: true
dependencies:
bindings: 1.5.0
prebuild-install: 7.1.2
dev: false
/better-sqlite3@9.6.0:
resolution: {integrity: sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ==}
requiresBuild: true

19
src/bank/db/queries.ts Normal file
View File

@ -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
)
}

View File

@ -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<OAuthTokenResponse> {
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,

32
src/bank/sparebank1Api.ts Normal file
View File

@ -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<T> = { status: "success"; data: T }
type Failure<T> = { status: "failure"; data: T }
type Result<OK, Err> = Success<OK> | Failure<Err>
function success<T>(data: T): Success<T> {
return { status: "success", data: data }
}
function failure<T>(data: T): Failure<T> {
return { status: "failure", data: data }
}
export async function refreshToken(
refreshToken: string,
): Promise<Result<OAuthTokenResponse, string>> {
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())
}

View File

@ -9,6 +9,7 @@ import { bankTransactionIntoActualTransaction } from "@/mappings.ts"
import { ACTUAL_ACCOUNT_IDS, BANK_ACCOUNT_IDS } from "../config.ts"
import logger from "@/logger.ts"
import type { UUID } from "node:crypto"
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
@ -48,16 +49,25 @@ async function fetchTransactionsFromPastDay(
async function main(): Promise<void> {
logger.info("Starting application")
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")
})
// logger.info("Shutting down")
// await actual.shutdown()
// db.close()
}
void main()