Compare commits

..

No commits in common. "3bf354b4bf642ff340d5ec0c859cf7ce61e0e6c0" and "6650e2cd2b0d21c1138d2af7d39f213b4d81e101" have entirely different histories.

8 changed files with 16 additions and 103 deletions

4
.gitignore vendored
View File

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

Binary file not shown.

View File

@ -4,7 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
"start": "dotenvx run --env-file=.env.local -- node --import=tsx ./src/main.ts | pino-pretty",
"start": "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,7 +14,6 @@
"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",
@ -23,7 +22,6 @@
},
"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,9 +11,6 @@ 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
@ -34,9 +31,6 @@ 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
@ -999,12 +993,6 @@ 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:
@ -1205,14 +1193,6 @@ 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

View File

@ -1,19 +0,0 @@
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,10 +1,11 @@
// TODO move types
import { BANK_INITIAL_REFRESH_TOKEN } from "@/../config.ts"
import {
BANK_INITIAL_REFRESH_TOKEN,
BANK_OAUTH_CLIENT_ID,
BANK_OAUTH_CLIENT_SECRET,
} 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
@ -48,11 +49,6 @@ 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
@ -87,15 +83,19 @@ export class Sparebank1Impl implements Sparebank1 {
async fetchNewRefreshToken(): Promise<OAuthTokenResponse> {
const refreshToken: string = await this.getRefreshToken()
const result = await Api.refreshToken(refreshToken)
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}`)
if (result.status === "failure") {
if (!response.ok) {
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,

View File

@ -1,32 +0,0 @@
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,7 +9,6 @@ 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
@ -49,25 +48,16 @@ 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(db))
await daily(actual, new Sparebank1Impl())
logger.info("Finished daily job")
})
// logger.info("Shutting down")
// await actual.shutdown()
// db.close()
}
void main()