🧹 Moved Sb1 API to separate workspace and created common workspace

- Created common workspace
- Create Sparebank1Api workspace
- Moved logger to common
- Moved SB1 types to types.ts
- Logger will avoid duplicating first line when capturing console.logs
- Updated imports and added type keyword
- Added nonUniqueId type
This commit is contained in:
2025-02-09 12:35:08 +01:00
parent efa9e785f2
commit c5b1ec20d6
23 changed files with 202 additions and 151 deletions

3
packages/common/date.ts Normal file
View File

@ -0,0 +1,3 @@
import { type Dayjs } from "dayjs"
export const toISODateString = (day: Dayjs): string => day.format("YYYY-MM-DD")

21
packages/common/logger.ts Normal file
View File

@ -0,0 +1,21 @@
import pino from "pino"
import { LOG_LEVEL } from "@/../config.ts"
/**
* / Returns a logging instance with the default log-level "info"
*/
const logger = pino(
pino.destination({
level: LOG_LEVEL,
}),
)
console.log = function (...args): void {
if (args.length > 1) {
logger.info(args?.slice(1), args?.[0])
} else {
logger.info(args?.[0])
}
}
export default logger

View File

@ -0,0 +1,12 @@
{
"name": "common",
"version": "1.0.0",
"description": "",
"license": "ISC",
"dependencies": {
"pino": "^9.6.0"
},
"devDependencies": {
"pino-pretty": "^13.0.0"
}
}

View File

@ -0,0 +1,12 @@
import type { Failure, Success } from "./types"
export const baseUrl = "https://api.sparebank1.no"
export const success = <T>(data: T): Success<T> => ({
status: "success",
data: data,
})
export const failure = <T>(data: T): Failure<T> => ({
status: "failure",
data: data,
})

View File

@ -0,0 +1,30 @@
import { OAuthTokenResponse, Result } from "./types"
import * as querystring from "node:querystring"
import { baseUrl, failure, success } from "./common"
import logger from "@common/logger"
export async function refreshToken(
clientId: string,
clientSecret: string,
refreshToken: string,
): Promise<Result<OAuthTokenResponse, string>> {
const queries = querystring.stringify({
client_id: clientId,
client_secret: clientSecret,
refresh_token: refreshToken,
grant_type: "refresh_token",
})
const url = `${baseUrl}/oauth/token?${queries}`
logger.debug(`Sending POST request to url: '${url}'`)
const response = await fetch(url, {
method: "post",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
})
logger.debug(`Received response with status '${response.status}'`)
if (!response.ok) {
return failure(await response.text())
}
return success(await response.json())
}

View File

@ -0,0 +1,6 @@
{
"name": "packages",
"version": "1.0.0",
"description": "",
"license": "ISC"
}

View File

@ -0,0 +1,35 @@
import type { Interval, TransactionResponse } from "./types"
import * as querystring from "node:querystring"
import { toISODateString } from "@common/date"
import logger from "@common/logger"
import { baseUrl } from "./common"
export async function list(
accessToken: string,
accountKeys: string | ReadonlyArray<string>,
interval?: Interval,
): Promise<TransactionResponse> {
const queryString = querystring.stringify({
accountKey: accountKeys,
...(interval && {
fromDate: toISODateString(interval.fromDate),
toDate: toISODateString(interval.toDate),
}),
})
const url = `${baseUrl}/personal/banking/transactions?${queryString}`
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

@ -0,0 +1,43 @@
import type { Dayjs } from "dayjs"
export type Success<T> = { status: "success"; data: T }
export type Failure<T> = { status: "failure"; data: T }
export type Result<OK, Err> = Success<OK> | Failure<Err>
export interface Interval {
fromDate: Dayjs
toDate: Dayjs
}
export interface OAuthTokenResponse {
access_token: string
expires_in: number
refresh_token_expires_in: number
refresh_token_absolute_expires_in: number
token_type: "Bearer"
refresh_token: string
}
export type BookingStatus = "PENDING" | "BOOKED"
export type NonUniqueId = "000000000000000000" | `${number}`
export interface SB1Transaction {
id: string
nonUniqueId: NonUniqueId
// The Id of the account
accountKey: string
// Unix time
date: number
// Amount in NOK
amount: number
cleanedDescription: string
remoteAccountName: string
bookingStatus: BookingStatus
[key: string]: string | number | boolean | unknown
}
export interface TransactionResponse {
transactions: ReadonlyArray<SB1Transaction>
}