v12.0.0 - initial commit

This commit is contained in:
padreug 2025-12-31 19:04:13 +01:00
commit e2c49ea43c
1145 changed files with 97211 additions and 0 deletions

View file

@ -0,0 +1,39 @@
{
"name": "typesafe-db",
"version": "12.0.0",
"license": "../LICENSE",
"type": "module",
"dependencies": {
"kysely": "^0.28.2",
"pg": "^8.16.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/pg": "^8.11.10",
"kysely-codegen": "^0.18.5",
"typescript": "^5.8.3"
},
"exports": {
".": {
"types": "./src/index.ts",
"default": "./lib/index.js"
}
},
"scripts": {
"build": "tsc --build",
"dev": "tsc --watch",
"generate-types": "kysely-codegen",
"postinstall": "npm run build"
},
"kysely-codegen": {
"camelCase": true,
"outFile": "./src/types/types.d.ts",
"overrides": {
"columns": {
"customers.id_card_data": "{firstName:string, lastName:string}",
"user_config.data": "{accounts?:object,config?:object}",
"edited_customer_data.id_card_data": "{firstName:string, lastName:string}"
}
}
}
}

View file

@ -0,0 +1,125 @@
import type { Insertable } from 'kysely'
import type { DBOrTx } from './db.js'
import type { ComplianceTriggers, RequirementType } from './types/types.js'
import { inTransaction } from './db.js'
import {
notifyUpdatedComplianceTriggerSets,
notifyUpdatedComplianceTriggers,
} from './notify.js'
type ComplianceTriggerInsert = Insertable<ComplianceTriggers>
/*
* Deprecated API
*/
export function getAllComplianceTriggers(dbOrTx: DBOrTx) {
return dbOrTx.selectFrom('complianceTriggers').selectAll().execute()
}
/*
* Compliance trigger sets API
*/
export function getComplianceTriggerSets(dbOrTx: DBOrTx) {
return dbOrTx.selectFrom('complianceTriggerSets').selectAll().execute()
}
export function getComplianceTriggerSetById(dbOrTx: DBOrTx, id: string) {
return dbOrTx
.selectFrom('complianceTriggerSets')
.where('id', '=', id)
.selectAll()
.executeTakeFirstOrThrow()
}
export function createComplianceTriggerSet(
dbOrTx: DBOrTx,
id: string,
name: string,
) {
return dbOrTx
.insertInto('complianceTriggerSets')
.values({ id, name })
.returningAll()
.executeTakeFirstOrThrow()
}
export function deleteComplianceTriggerSet(dbOrTx: DBOrTx, id: string) {
return inTransaction(async tx => {
const complianceTriggerSet = await tx
.deleteFrom('complianceTriggerSets')
.where('id', '=', id)
.returningAll()
.executeTakeFirstOrThrow()
await notifyUpdatedComplianceTriggerSets(tx)
return complianceTriggerSet
}, dbOrTx)
}
/*
* Compliance triggers API with support for compliance trigger sets
*/
export function getComplianceTriggers(
dbOrTx: DBOrTx,
complianceTriggerSetId: string,
) {
return dbOrTx
.selectFrom('complianceTriggers')
.selectAll()
.where('complianceTriggerSetId', '=', complianceTriggerSetId)
.execute()
}
export function createComplianceTrigger(
dbOrTx: DBOrTx,
complianceTriggerSetId: string,
trigger: ComplianceTriggerInsert[],
) {
return inTransaction(async tx => {
const complianceTrigger = await tx
.insertInto('complianceTriggers')
.values(Object.assign({}, trigger, { complianceTriggerSetId }))
.execute()
await notifyUpdatedComplianceTriggers(tx)
return complianceTrigger
}, dbOrTx)
}
export function deleteComplianceTrigger(dbOrTx: DBOrTx, id: string) {
return inTransaction(async tx => {
const complianceTrigger = await tx
.deleteFrom('complianceTriggers')
.where('id', '=', id)
.execute()
await notifyUpdatedComplianceTriggers(tx)
return complianceTrigger
}, dbOrTx)
}
export function deleteComplianceTriggersByCustomInfoRequestId(
dbOrTx: DBOrTx,
customInfoRequestId: string,
) {
return inTransaction(async tx => {
const complianceTrigger = await tx
.deleteFrom('complianceTriggers')
.where('customInfoRequestId', '=', customInfoRequestId)
.execute()
await notifyUpdatedComplianceTriggers(tx)
return complianceTrigger
}, dbOrTx)
}
export function getAllComplianceTriggersByRequirementType(
dbOrTx: DBOrTx,
requirementType: RequirementType,
) {
return dbOrTx
.selectFrom('complianceTriggers')
.selectAll()
.where('requirementType', '=', requirementType)
.execute()
}

View file

@ -0,0 +1,216 @@
import { sql } from 'kysely'
import db from './db.js'
import { jsonArrayFrom } from 'kysely/helpers/postgres'
import type {
CustomerEB,
CustomerWithEditedDataEB,
} from './types/manual.types.js'
const ANON_ID = '47ac1184-8102-11e7-9079-8f13a7117867'
const TX_PASSTHROUGH_ERROR_CODES = [
'operatorCancel',
'scoreThresholdReached',
'walletScoringError',
]
function transactionUnion(eb: CustomerEB) {
return eb
.selectFrom('cashInTxs')
.select([
'created',
'fiat',
'fiatCode',
'errorCode',
eb.val('cashIn').as('txClass'),
])
.where(({ eb, and, or, ref }) =>
and([
eb('customerId', '=', ref('cst.id')),
or([eb('sendConfirmed', '=', true), eb('batched', '=', true)]),
]),
)
.unionAll(
eb
.selectFrom('cashOutTxs')
.select([
'created',
'fiat',
'fiatCode',
'errorCode',
eb.val('cashOut').as('txClass'),
])
.where(({ eb, and, ref }) =>
and([
eb('customerId', '=', ref('cst.id')),
eb('confirmedAt', 'is not', null),
]),
),
)
}
function joinLatestTx(eb: CustomerEB) {
return eb
.selectFrom(eb =>
transactionUnion(eb).orderBy('created', 'desc').limit(1).as('lastTx'),
)
.select(['fiatCode', 'fiat', 'txClass', 'created'])
.as('lastTx')
}
function joinTxsTotals(eb: CustomerEB) {
return eb
.selectFrom(eb => transactionUnion(eb).as('combinedTxs'))
.select([
eb => eb.fn.coalesce(eb.fn.countAll(), eb.val(0)).as('totalTxs'),
eb =>
eb.fn
.coalesce(
eb.fn.sum(
eb
.case()
.when(
eb.or([
eb('combinedTxs.errorCode', 'is', null),
eb(
'combinedTxs.errorCode',
'not in',
TX_PASSTHROUGH_ERROR_CODES,
),
]),
)
.then(eb.ref('combinedTxs.fiat'))
.else(0)
.end(),
),
eb.val(0),
)
.as('totalSpent'),
])
.as('txStats')
}
function selectNewestIdCardData({ eb, ref }: CustomerWithEditedDataEB) {
return eb
.case()
.when(
eb.and([
eb(ref('cstED.idCardDataAt'), 'is not', null),
eb.or([
eb(ref('cst.idCardDataAt'), 'is', null),
eb(ref('cstED.idCardDataAt'), '>', ref('cst.idCardDataAt')),
]),
]),
)
.then(ref('cstED.idCardData'))
.else(ref('cst.idCardData'))
.end()
}
interface GetCustomerListOptions {
withCustomInfoRequest: boolean
}
const defaultOptions: GetCustomerListOptions = {
withCustomInfoRequest: false,
}
// TODO left join lateral is having issues deriving type
function getCustomerList(
options: GetCustomerListOptions = defaultOptions,
): Promise<any[]> {
return db
.selectFrom('customers as cst')
.leftJoin('editedCustomerData as cstED', 'cstED.customerId', 'cst.id')
.leftJoinLateral(joinTxsTotals, join => join.onTrue())
.leftJoinLateral(joinLatestTx, join => join.onTrue())
.select(({ eb, fn, val }) => [
'cst.id',
'cst.phone',
'cst.authorizedOverride',
'cst.frontCameraPath',
'cst.frontCameraOverride',
'cst.idCardPhotoPath',
'cst.idCardPhotoOverride',
selectNewestIdCardData(eb).as('idCardData'),
'cst.idCardDataOverride',
'cst.email',
'cst.usSsn',
'cst.usSsnOverride',
'cst.sanctions',
'cst.sanctionsOverride',
'txStats.totalSpent',
'txStats.totalTxs',
'lastTx.fiatCode as lastTxFiatCode',
'lastTx.fiat as lastTxFiat',
'lastTx.txClass as lastTxClass',
fn<Date>('GREATEST', [
'cst.created',
'lastTx.created',
'cst.phoneAt',
'cst.emailAt',
'cst.idCardDataAt',
'cst.frontCameraAt',
'cst.idCardPhotoAt',
'cst.usSsnAt',
'cst.lastAuthAttempt',
]).as('lastActive'),
eb('cst.suspendedUntil', '>', fn<Date>('NOW', [])).as('isSuspended'),
fn<number>('GREATEST', [
val(0),
fn<number>('date_part', [
val('day'),
eb('cst.suspendedUntil', '-', fn<Date>('NOW', [])),
]),
]).as('daysSuspended'),
])
.where('cst.id', '!=', ANON_ID)
.$if(options.withCustomInfoRequest, qb =>
qb.select(({ eb, ref }) =>
jsonArrayFrom(
eb
.selectFrom('customersCustomInfoRequests')
.selectAll()
.where('customerId', '=', ref('cst.id')),
).as('customInfoRequestData'),
),
)
.orderBy('lastActive', 'desc')
.execute()
}
function searchCustomers(searchTerm: string, limit: number = 20): Promise<any> {
const searchPattern = `%${searchTerm}%`
return db
.selectFrom(
db
.selectFrom('customers as cst')
.leftJoin('editedCustomerData as cstED', 'cstED.customerId', 'cst.id')
.select(({ eb }) => [
'cst.id',
'cst.phone',
'cst.email',
sql`TRIM(CONCAT(
COALESCE(${selectNewestIdCardData(eb)}->>'firstName', ''),
' ',
COALESCE(${selectNewestIdCardData(eb)}->>'lastName', '')
))`.as('customerName'),
])
.where('cst.id', '!=', ANON_ID)
.as('customers_with_names'),
)
.selectAll()
.select('customerName as name')
.where(({ eb, or }) =>
or([
eb('phone', 'ilike', searchPattern),
eb('email', 'ilike', searchPattern),
eb('customerName', 'ilike', searchPattern),
]),
)
.orderBy('id')
.limit(limit)
.execute()
}
export { getCustomerList, selectNewestIdCardData, searchCustomers }

View file

@ -0,0 +1,42 @@
import type { DB } from './types/types.js'
import { Pool } from 'pg'
import { Kysely, PostgresDialect, CamelCasePlugin } from 'kysely'
const POSTGRES_USER = process.env.POSTGRES_USER
const POSTGRES_PASSWORD = process.env.POSTGRES_PASSWORD
const POSTGRES_HOST = process.env.POSTGRES_HOST
const POSTGRES_PORT = process.env.POSTGRES_PORT
const POSTGRES_DB = process.env.POSTGRES_DB
const PSQL_URL = `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}`
const dialect = new PostgresDialect({
pool: new Pool({
connectionString: PSQL_URL,
max: 5,
}),
})
export type DBOrTx = Kysely<DB>
const db: Kysely<DB> = new Kysely<DB>({
dialect,
plugins: [
new CamelCasePlugin({
maintainNestedObjectKeys: true,
underscoreBeforeDigits: true,
}),
],
})
export default db
export function inTransaction<DB, T>(
func: (tx: Kysely<DB>) => Promise<T>,
dbOrTx: Kysely<DB>, // TODO: default to `db`
): Promise<T> {
return dbOrTx.isTransaction
? func(dbOrTx)
: dbOrTx.transaction().execute(func)
}

View file

@ -0,0 +1,9 @@
export * as db from './db.js'
export * as notify from './notify.js'
export * as customers from './customers.js'
export * as complianceTriggers from './compliance-triggers.js'
export * as transactions from './transactions.js'
export * as machineGroups from './machine-groups.js'
export * as machines from './machines.js'
export * as userConfig from './user-config.js'
export { PG_ERROR_CODES } from './pg-error-codes.js'

View file

@ -0,0 +1,30 @@
export function logQuery(compiledQuery: {
sql: string
parameters: readonly unknown[]
}) {
const { sql, parameters } = compiledQuery
let interpolatedSql = sql
let paramIndex = 0
interpolatedSql = sql.replace(/\$\d+|\?/g, () => {
const param = parameters[paramIndex++]
if (param === null || param === undefined) {
return 'NULL'
} else if (typeof param === 'string') {
return `'${param.replace(/'/g, "''")}'`
} else if (typeof param === 'boolean') {
return param.toString()
} else if (param instanceof Date) {
return `'${param.toISOString()}'`
} else if (typeof param === 'object') {
return `'${JSON.stringify(param).replace(/'/g, "''")}'`
} else {
return String(param)
}
})
console.log('📝 Query:', interpolatedSql)
return interpolatedSql
}

View file

@ -0,0 +1,64 @@
import type { DBOrTx } from './db.js'
import db, { inTransaction } from './db.js'
import { notifyUpdatedComplianceTriggerSets } from './notify.js'
export function createMachineGroup(data: {
id: string
name: string
complianceTriggerSetId: string | null
}) {
return db
.insertInto('machineGroups')
.values(data)
.returningAll()
.executeTakeFirstOrThrow()
}
export function deleteMachineGroup(id: string) {
return db
.deleteFrom('machineGroups')
.where('id', '=', id)
.returningAll()
.executeTakeFirstOrThrow()
}
export function getMachineGroupsWithDeviceCount() {
return db
.selectFrom('machineGroups as mg')
.leftJoin('devices as d', 'd.machineGroupId', 'mg.id')
.select([
'mg.id',
'mg.name',
'mg.complianceTriggerSetId',
eb => eb.fn.count('d.deviceId').as('deviceCount'),
])
.groupBy(['mg.id', 'mg.name'])
.orderBy(eb =>
eb.case().when('mg.name', '=', 'default').then(0).else(1).end(),
)
.orderBy('mg.name', 'asc')
.execute()
}
export function setComplianceTriggerSetId(
id: string,
complianceTriggerSetId: string | null,
) {
return inTransaction(async tx => {
const machineGroup = await tx
.updateTable('machineGroups')
.set({ complianceTriggerSetId })
.where('id', '=', id)
.returningAll()
.executeTakeFirstOrThrow()
await notifyUpdatedComplianceTriggerSets(tx)
return machineGroup
}, db)
}
export function getMachineGroupsComplianceTriggerSets(dbOrTx: DBOrTx) {
return dbOrTx
.selectFrom('machineGroups')
.select(['id', 'complianceTriggerSetId'])
.execute()
}

View file

@ -0,0 +1,35 @@
import type { DBOrTx } from './db.js'
import db, { inTransaction } from './db.js'
import { notifyUpdatedMachineGroups } from './notify.js'
export function getMachinesGroups(dbOrTx: DBOrTx) {
return dbOrTx
.selectFrom('devices as d')
.select(['deviceId', 'machineGroupId'])
.where('paired', '=', true)
.execute()
}
export function assignMachinesToGroup(deviceIds: [string], groupId: string) {
return inTransaction(async tx => {
const machines = await tx
.updateTable('devices as d')
.set({ machineGroupId: groupId })
.where('d.deviceId', 'in', deviceIds)
.execute()
await notifyUpdatedMachineGroups(tx)
return machines
}, db)
}
export async function getHighestRestrictionLevel(
dbOrTx: DBOrTx = db,
): Promise<number> {
const result = await dbOrTx
.selectFrom('devices')
.select(db => db.fn.max('restrictionLevel').as('maxRestrictionLevel'))
.where('paired', '=', true)
.executeTakeFirst()
return result?.maxRestrictionLevel ?? 0
}

View file

@ -0,0 +1,24 @@
import { sql } from 'kysely'
import type { DBOrTx } from './db.js'
function notify(dbOrTx: DBOrTx, channel: string) {
const sqlChannel = sql.id(channel)
return sql`NOTIFY ${sqlChannel}`.execute(dbOrTx)
}
export function notifyReload(dbOrTx: DBOrTx) {
return notify(dbOrTx, 'reload')
}
export function notifyUpdatedMachineGroups(dbOrTx: DBOrTx) {
return notify(dbOrTx, 'updated_machine_groups')
}
export function notifyUpdatedComplianceTriggerSets(dbOrTx: DBOrTx) {
return notify(dbOrTx, 'updated_compliance_trigger_sets')
}
export function notifyUpdatedComplianceTriggers(dbOrTx: DBOrTx) {
return notify(dbOrTx, 'updated_compliance_triggers')
}

View file

@ -0,0 +1,4 @@
export const PG_ERROR_CODES = {
UNIQUE_VIOLATION: '23505',
FOREIGN_KEY_VIOLATION: '23503',
} as const

View file

@ -0,0 +1,389 @@
import { sql } from 'kysely'
import db from './db.js'
import type {
CashInWithBatchEB,
CashOutEB,
CustomerWithEditedDataEB,
DevicesAndUnpairedDevicesEB,
} from './types/manual.types.js'
import { selectNewestIdCardData } from './customers.js'
const PENDING_INTERVAL = '60 minutes'
const REDEEMABLE_INTERVAL = '24 hours'
function getDeviceName(eb: DevicesAndUnpairedDevicesEB) {
return eb
.case()
.when(eb('ud.name', 'is not', null))
.then(eb('ud.name', '||', ' (unpaired)'))
.when(eb('d.name', 'is not', null))
.then(eb.ref('d.name'))
.else('Unpaired')
.end()
}
function customerData({ eb, ref }: CustomerWithEditedDataEB) {
return [
ref('cst.phone').as('customerPhone'),
ref('cst.email').as('customerEmail'),
selectNewestIdCardData(eb).as('customerIdCardData'),
ref('cst.frontCameraPath').as('customerFrontCameraPath'),
ref('cst.idCardPhotoPath').as('customerIdCardPhotoPath'),
ref('cst.isTestCustomer').as('isTestCustomer'),
]
}
function isCashInExpired(eb: CashInWithBatchEB) {
return eb.and([
eb.not('txIn.sendConfirmed'),
eb(
'txIn.created',
'<=',
sql<Date>`now() - interval '${sql.raw(PENDING_INTERVAL)}'`,
),
])
}
function isCashOutExpired(eb: CashOutEB) {
return eb.and([
eb.not('txOut.dispense'),
eb(
eb.fn.coalesce('txOut.confirmed_at', 'txOut.created'),
'<=',
sql<Date>`now() - interval '${sql.raw(REDEEMABLE_INTERVAL)}'`,
),
])
}
function cashOutTransactionStates(eb: CashOutEB) {
return eb
.case()
.when(eb('txOut.error', '=', eb.val('Operator cancel')))
.then('Cancelled')
.when(eb('txOut.error', 'is not', null))
.then('Error')
.when(eb.ref('txOut.dispense'))
.then('Success')
.when(isCashOutExpired(eb))
.then('Expired')
.else('Pending')
.end()
}
function cashInTransactionStates(eb: CashInWithBatchEB) {
const operatorCancel = eb.and([
eb.ref('txIn.operatorCompleted'),
eb('txIn.error', '=', eb.val('Operator cancel')),
])
const hasError = eb.or([
eb('txIn.error', 'is not', null),
eb('txInB.errorMessage', 'is not', null),
])
return eb
.case()
.when(operatorCancel)
.then('Cancelled')
.when(hasError)
.then('Error')
.when(eb.ref('txIn.sendConfirmed'))
.then('Sent')
.when(isCashInExpired(eb))
.then('Expired')
.else('Pending')
.end()
}
function getCashOutTransactionList() {
return db
.selectFrom('cashOutTxs as txOut')
.leftJoin('customers as cst', 'cst.id', 'txOut.customerId')
.leftJoin('editedCustomerData as cstED', 'cst.id', 'cstED.customerId')
.innerJoin('cashOutActions as txOutActions', join =>
join
.onRef('txOut.id', '=', 'txOutActions.txId')
.on('txOutActions.action', '=', 'provisionAddress'),
)
.leftJoin('devices as d', 'd.deviceId', 'txOut.deviceId')
.leftJoin('unpairedDevices as ud', join =>
join
.onRef('txOut.deviceId', '=', 'ud.deviceId')
.on('ud.unpaired', '>=', eb => eb.ref('txOut.created'))
.on('txOut.created', '>=', eb => eb.ref('ud.paired')),
)
.leftJoin('coupons as cpn', 'cpn.id', 'txOut.couponId')
.select(({ eb, val }) => [
'txOut.id',
val('cashOut').as('txClass'),
'txOut.deviceId',
'txOut.toAddress',
'txOut.cryptoAtoms',
'txOut.cryptoCode',
'txOut.fiat',
'txOut.fiatCode',
'txOut.phone', // TODO why does this has phone? Why not get from customer?
'txOut.error',
'txOut.created',
'txOut.timedout',
'txOut.errorCode',
'txOut.fixedFee',
'txOut.txVersion',
'txOut.termsAccepted',
'txOut.commissionPercentage',
'txOut.rawTickerPrice',
isCashOutExpired(eb).as('expired'),
getDeviceName(eb).as('machineName'),
'txOut.discount',
'cpn.code as couponCode',
cashOutTransactionStates(eb).as('status'),
'txOut.customerId',
...customerData(eb),
'txOut.txCustomerPhotoPath',
'txOut.txCustomerPhotoAt',
'txOut.walletScore',
// cash-in only
val(null).as('fee'),
val(null).as('txHash'),
val(false).as('send'),
val(false).as('sendConfirmed'),
val(null).as('sendTime'),
val(false).as('operatorCompleted'),
val(false).as('sendPending'),
val(0).as('minimumTx'),
val(null).as('isPaperWallet'),
val(false).as('batched'),
val(null).as('batchTime'),
val(null).as('batchError'),
// cash-out only
'txOut.dispense',
'txOut.swept',
'txOut.denominationRecycler1',
'txOut.denominationRecycler2',
'txOut.denominationRecycler3',
'txOut.denominationRecycler4',
'txOut.denominationRecycler5',
'txOut.denominationRecycler6',
'txOut.denomination1',
'txOut.denomination2',
'txOut.denomination3',
'txOut.denomination4',
'txOut.provisioned1',
'txOut.provisioned2',
'txOut.provisioned3',
'txOut.provisioned4',
'txOut.provisionedRecycler1',
'txOut.provisionedRecycler2',
'txOut.provisionedRecycler3',
'txOut.provisionedRecycler4',
'txOut.provisionedRecycler5',
'txOut.provisionedRecycler6',
'txOut.dispenseConfirmed',
'txOut.publishedAt',
'txOut.hdIndex',
'txOut.notified',
'txOut.receivedCryptoAtoms',
'txOut.redeem',
'txOut.confirmedAt',
])
}
function getCashInTransactionList() {
return db
.selectFrom('cashInTxs as txIn')
.leftJoin('customers as cst', 'cst.id', 'txIn.customerId')
.leftJoin('editedCustomerData as cstED', 'cst.id', 'cstED.customerId')
.leftJoin('transactionBatches as txInB', 'txInB.id', 'txIn.batchId')
.leftJoin('devices as d', 'd.deviceId', 'txIn.deviceId')
.leftJoin('unpairedDevices as ud', join =>
join
.onRef('txIn.deviceId', '=', 'ud.deviceId')
.on('ud.unpaired', '>=', eb => eb.ref('txIn.created'))
.on('txIn.created', '>=', eb => eb.ref('ud.paired')),
)
.leftJoin('coupons as cpn', 'cpn.id', 'txIn.couponId')
.select(({ eb, val }) => [
'txIn.id',
val('cashIn').as('txClass'),
'txIn.deviceId',
'txIn.toAddress',
'txIn.cryptoAtoms',
'txIn.cryptoCode',
'txIn.fiat',
'txIn.fiatCode',
'txIn.phone', // TODO why does this has phone? Why not get from customer?
'txIn.error',
'txIn.created',
'txIn.timedout',
'txIn.errorCode',
'txIn.cashInFee as fixedFee',
'txIn.txVersion',
'txIn.termsAccepted',
'txIn.commissionPercentage',
'txIn.rawTickerPrice',
isCashInExpired(eb).as('expired'),
getDeviceName(eb).as('machineName'),
'txIn.discount',
'cpn.code as couponCode',
cashInTransactionStates(eb).as('status'),
'txIn.customerId',
...customerData(eb),
'txIn.txCustomerPhotoPath',
'txIn.txCustomerPhotoAt',
'txIn.walletScore',
// cash-in only
'txIn.fee',
'txIn.txHash',
'txIn.send',
'txIn.sendConfirmed',
'txIn.sendTime',
'txIn.operatorCompleted',
'txIn.sendPending',
'txIn.minimumTx',
'txIn.isPaperWallet',
'txInB.errorMessage as batchError',
'txIn.batched',
'txIn.batchTime',
// cash-out only
val(false).as('dispense'),
val(false).as('swept'),
eb.val<number | null>(null).as('denominationRecycler1'),
eb.val<number | null>(null).as('denominationRecycler2'),
eb.val<number | null>(null).as('denominationRecycler3'),
eb.val<number | null>(null).as('denominationRecycler4'),
eb.val<number | null>(null).as('denominationRecycler5'),
eb.val<number | null>(null).as('denominationRecycler6'),
eb.val<number | null>(null).as('denomination1'),
eb.val<number | null>(null).as('denomination2'),
eb.val<number | null>(null).as('denomination3'),
eb.val<number | null>(null).as('denomination4'),
eb.val<number | null>(null).as('provisioned1'),
eb.val<number | null>(null).as('provisioned2'),
eb.val<number | null>(null).as('provisioned3'),
eb.val<number | null>(null).as('provisioned4'),
eb.val<number | null>(null).as('provisionedRecycler1'),
eb.val<number | null>(null).as('provisionedRecycler2'),
eb.val<number | null>(null).as('provisionedRecycler3'),
eb.val<number | null>(null).as('provisionedRecycler4'),
eb.val<number | null>(null).as('provisionedRecycler5'),
eb.val<number | null>(null).as('provisionedRecycler6'),
eb.val<boolean | null>(null).as('dispenseConfirmed'),
eb.val<Date | null>(null).as('publishedAt'),
eb.val<number | null>(null).as('hdIndex'),
eb.val<boolean>(false).as('notified'),
eb.val<string | null>(null).as('receivedCryptoAtoms'),
eb.val<boolean>(false).as('redeem'),
eb.val<Date | null>(null).as('confirmedAt'),
])
}
interface PaginationParams {
limit?: number
offset?: number
}
interface FilterParams {
from?: Date
until?: Date
toAddress?: string
txClass?: string
deviceId?: string
customerId?: string
cryptoCode?: string
swept?: boolean
status?: string
excludeTestingCustomers?: boolean
}
async function getTransactionById(id: string) {
let query = db.selectFrom(() =>
getCashInTransactionList()
.unionAll(getCashOutTransactionList())
.as('transactions'),
)
query = query.selectAll('transactions').where('transactions.id', '=', id)
return query.executeTakeFirst()
}
async function getTransactionList(
filters: FilterParams,
pagination?: PaginationParams,
) {
let query = db
.selectFrom(() =>
getCashInTransactionList()
.unionAll(getCashOutTransactionList())
.as('transactions'),
)
.selectAll('transactions')
.select(eb =>
sql<{
totalCount: number
}>`json_build_object(${sql.lit('totalCount')}, ${eb.fn.count('transactions.id').over()})`.as(
'paginationStats',
),
)
.orderBy('transactions.created', 'desc')
if (filters.toAddress) {
query = query.where(
'transactions.toAddress',
'like',
`%${filters.toAddress}%`,
)
}
if (filters.from) {
query = query.where('transactions.created', '>=', filters.from)
}
if (filters.until) {
query = query.where('transactions.created', '<=', filters.until)
}
if (filters.deviceId) {
query = query.where('transactions.deviceId', '=', filters.deviceId)
}
if (filters.txClass) {
query = query.where('transactions.txClass', '=', filters.txClass)
}
if (filters.customerId) {
query = query.where('transactions.customerId', '=', filters.customerId)
}
if (filters.cryptoCode) {
query = query.where('transactions.cryptoCode', '=', filters.cryptoCode)
}
if (filters.swept) {
query = query.where('transactions.swept', '=', filters.swept)
}
if (filters.status) {
query = query.where('transactions.status', '=', filters.status)
}
if (filters.excludeTestingCustomers) {
query = query.where('transactions.isTestCustomer', '=', false)
}
if (pagination?.limit) {
query = query.limit(pagination.limit)
}
if (pagination?.offset) {
query = query.offset(pagination.offset)
}
return query.execute()
}
export {
getTransactionList,
getCashInTransactionList,
getCashOutTransactionList,
getTransactionById,
}

View file

@ -0,0 +1,35 @@
import type { ExpressionBuilder } from 'kysely'
import type {
CashInTxs,
Customers,
DB,
Devices,
EditedCustomerData,
TransactionBatches,
UnpairedDevices,
} from './types.js'
import type { Nullable } from 'kysely/dist/esm/index.js'
export type CustomerEB = ExpressionBuilder<DB & { cst: Customers }, 'cst'>
export type CustomerWithEditedDataEB = ExpressionBuilder<
DB & { cst: Customers } & { cstED: EditedCustomerData },
'cst' | 'cstED'
>
export type CashInEB = ExpressionBuilder<DB & { txIn: CashInTxs }, 'txIn'>
export type CashInWithBatchEB = ExpressionBuilder<
DB & { txIn: CashInTxs } & {
txInB: TransactionBatches
},
'txIn' | 'txInB'
>
export type CashOutEB = ExpressionBuilder<DB & { txOut: CashOutTxs }, 'txOut'>
export type DevicesAndUnpairedDevicesEB = ExpressionBuilder<
DB & { d: Nullable<Devices> } & {
ud: Nullable<UnpairedDevices>
},
'd' | 'ud'
>
export type GenericEB = ExpressionBuilder<DB, any>

View file

@ -0,0 +1,796 @@
/**
* This file was generated by kysely-codegen.
* Please do not edit it manually.
*/
import type { ColumnType } from 'kysely'
export type AuthTokenType = 'reset_password' | 'reset_twofa'
export type CashUnit =
| 'cashbox'
| 'cassette1'
| 'cassette2'
| 'cassette3'
| 'cassette4'
| 'recycler1'
| 'recycler2'
| 'recycler3'
| 'recycler4'
| 'recycler5'
| 'recycler6'
export type CashUnitOperationType =
| 'cash-box-empty'
| 'cash-box-refill'
| 'cash-cassette-1-count-change'
| 'cash-cassette-1-empty'
| 'cash-cassette-1-refill'
| 'cash-cassette-2-count-change'
| 'cash-cassette-2-empty'
| 'cash-cassette-2-refill'
| 'cash-cassette-3-count-change'
| 'cash-cassette-3-empty'
| 'cash-cassette-3-refill'
| 'cash-cassette-4-count-change'
| 'cash-cassette-4-empty'
| 'cash-cassette-4-refill'
| 'cash-recycler-1-count-change'
| 'cash-recycler-1-empty'
| 'cash-recycler-1-refill'
| 'cash-recycler-2-count-change'
| 'cash-recycler-2-empty'
| 'cash-recycler-2-refill'
| 'cash-recycler-3-count-change'
| 'cash-recycler-3-empty'
| 'cash-recycler-3-refill'
| 'cash-recycler-4-count-change'
| 'cash-recycler-4-empty'
| 'cash-recycler-4-refill'
| 'cash-recycler-5-count-change'
| 'cash-recycler-5-empty'
| 'cash-recycler-5-refill'
| 'cash-recycler-6-count-change'
| 'cash-recycler-6-empty'
| 'cash-recycler-6-refill'
export type ComplianceTriggerDirection = 'both' | 'cashIn' | 'cashOut'
export type ComplianceType =
| 'authorized'
| 'front_camera'
| 'hard_limit'
| 'id_card_data'
| 'id_card_photo'
| 'sanctions'
| 'sms'
| 'us_ssn'
export type DiscountSource = 'individualDiscount' | 'promoCode'
export type ExternalComplianceStatus =
| 'APPROVED'
| 'PENDING'
| 'REJECTED'
| 'RETRY'
export type Generated<T> =
T extends ColumnType<infer S, infer I, infer U>
? ColumnType<S, I | undefined, U>
: ColumnType<T, T | undefined, T>
export type Int8 = ColumnType<
string,
bigint | number | string,
bigint | number | string
>
export type Json = JsonValue
export type JsonArray = JsonValue[]
export type JsonObject = {
[x: string]: JsonValue | undefined
}
export type JsonPrimitive = boolean | number | string | null
export type JsonValue = JsonArray | JsonObject | JsonPrimitive
export type NotificationType =
| 'compliance'
| 'cryptoBalance'
| 'error'
| 'fiatBalance'
| 'highValueTransaction'
| 'security'
| 'transaction'
export type Numeric = ColumnType<string, number | string, number | string>
export type RequirementType =
| 'block'
| 'custom'
| 'external'
| 'facephoto'
| 'idCardData'
| 'idCardPhoto'
| 'sanctions'
| 'sms'
| 'suspend'
| 'usSsn'
export type Role = 'superuser' | 'user'
export type SmsNoticeEvent =
| 'cash_out_dispense_ready'
| 'sms_code'
| 'sms_receipt'
export type StatusStage =
| 'authorized'
| 'confirmed'
| 'instant'
| 'insufficientFunds'
| 'notSeen'
| 'published'
| 'rejected'
export type Timestamp = ColumnType<Date, Date | string, Date | string>
export type TradeType = 'buy' | 'sell'
export type TransactionBatchStatus = 'failed' | 'open' | 'ready' | 'sent'
export type TriggerType =
| 'consecutiveDays'
| 'txAmount'
| 'txVelocity'
| 'txVolume'
export type VerificationType = 'automatic' | 'blocked' | 'verified'
export interface AuthTokens {
expire: Generated<Timestamp>
token: string
type: AuthTokenType
userId: string | null
}
export interface Bills {
cashboxBatchId: string | null
cashInFee: Numeric
cashInTxsId: string
created: Generated<Timestamp>
cryptoCode: Generated<string | null>
destinationUnit: Generated<CashUnit>
deviceTime: Int8
fiat: number
fiatCode: string
id: string
legacy: Generated<boolean | null>
}
export interface Blacklist {
address: string
blacklistMessageId: Generated<string>
}
export interface BlacklistMessages {
allowToggle: Generated<boolean>
content: string
id: string
label: string
}
export interface CashInActions {
action: string
created: Generated<Timestamp>
error: string | null
errorCode: string | null
id: Generated<number>
txHash: string | null
txId: string
}
export interface CashInTxs {
batched: Generated<boolean>
batchId: string | null
batchTime: Timestamp | null
cashInFee: Numeric
commissionPercentage: Generated<Numeric | null>
couponId: string | null
created: Generated<Timestamp>
cryptoAtoms: Numeric
cryptoCode: string
customerId: Generated<string | null>
deviceId: string
discount: number | null
discountSource: DiscountSource | null
email: string | null
error: string | null
errorCode: string | null
fee: Int8 | null
fiat: Numeric
fiatCode: string
id: string
isPaperWallet: Generated<boolean | null>
minimumTx: number
operatorCompleted: Generated<boolean>
phone: string | null
rawTickerPrice: Generated<Numeric | null>
send: Generated<boolean>
sendConfirmed: Generated<boolean>
sendPending: Generated<boolean>
sendTime: Timestamp | null
termsAccepted: Generated<boolean>
timedout: Generated<boolean>
toAddress: string
txCustomerPhotoAt: Timestamp | null
txCustomerPhotoPath: string | null
txHash: string | null
txVersion: number
walletScore: number | null
}
export interface CashinTxTrades {
tradeId: Generated<number>
txId: string
}
export interface CashOutActions {
action: string
created: Generated<Timestamp>
denomination1: number | null
denomination2: number | null
denomination3: number | null
denomination4: number | null
denominationRecycler1: number | null
denominationRecycler2: number | null
denominationRecycler3: number | null
denominationRecycler4: number | null
denominationRecycler5: number | null
denominationRecycler6: number | null
deviceId: Generated<string>
deviceTime: Int8 | null
dispensed1: number | null
dispensed2: number | null
dispensed3: number | null
dispensed4: number | null
dispensedRecycler1: number | null
dispensedRecycler2: number | null
dispensedRecycler3: number | null
dispensedRecycler4: number | null
dispensedRecycler5: number | null
dispensedRecycler6: number | null
error: string | null
errorCode: string | null
id: Generated<number>
layer2Address: string | null
provisioned1: number | null
provisioned2: number | null
provisioned3: number | null
provisioned4: number | null
provisionedRecycler1: number | null
provisionedRecycler2: number | null
provisionedRecycler3: number | null
provisionedRecycler4: number | null
provisionedRecycler5: number | null
provisionedRecycler6: number | null
redeem: Generated<boolean>
rejected1: number | null
rejected2: number | null
rejected3: number | null
rejected4: number | null
rejectedRecycler1: number | null
rejectedRecycler2: number | null
rejectedRecycler3: number | null
rejectedRecycler4: number | null
rejectedRecycler5: number | null
rejectedRecycler6: number | null
toAddress: string | null
txHash: string | null
txId: string
}
export interface CashOutTxs {
commissionPercentage: Generated<Numeric | null>
confirmedAt: Timestamp | null
couponId: string | null
created: Generated<Timestamp>
cryptoAtoms: Numeric
cryptoCode: string
customerId: Generated<string | null>
denomination1: number | null
denomination2: number | null
denomination3: number | null
denomination4: number | null
denominationRecycler1: number | null
denominationRecycler2: number | null
denominationRecycler3: number | null
denominationRecycler4: number | null
denominationRecycler5: number | null
denominationRecycler6: number | null
deviceId: string
discount: number | null
discountSource: DiscountSource | null
dispense: Generated<boolean>
dispenseConfirmed: Generated<boolean | null>
email: string | null
error: string | null
errorCode: string | null
fiat: Numeric
fiatCode: string
fixedFee: Generated<Numeric>
hdIndex: Generated<number | null>
id: string
layer2Address: string | null
notified: Generated<boolean>
phone: string | null
provisioned1: number | null
provisioned2: number | null
provisioned3: number | null
provisioned4: number | null
provisionedRecycler1: number | null
provisionedRecycler2: number | null
provisionedRecycler3: number | null
provisionedRecycler4: number | null
provisionedRecycler5: number | null
provisionedRecycler6: number | null
publishedAt: Timestamp | null
rawTickerPrice: Generated<Numeric | null>
receivedCryptoAtoms: Generated<Numeric | null>
redeem: Generated<boolean>
status: Generated<StatusStage>
swept: Generated<boolean>
termsAccepted: Generated<boolean>
timedout: Generated<boolean>
toAddress: string
txCustomerPhotoAt: Timestamp | null
txCustomerPhotoPath: string | null
txVersion: number
walletScore: number | null
}
export interface CashoutTxTrades {
tradeId: Generated<number>
txId: string
}
export interface CashUnitOperation {
billCountOverride: number | null
created: Generated<Timestamp>
deviceId: string | null
id: string
operationType: CashUnitOperationType
performedBy: string | null
}
export interface ComplianceOverrides {
complianceType: ComplianceType
customerId: string | null
id: string
overrideAt: Timestamp
overrideBy: string | null
verification: VerificationType
}
export interface ComplianceTriggers {
complianceTriggerSetId: string
customInfoRequestId: string | null
direction: ComplianceTriggerDirection
externalService: string | null
id: string
requirementType: RequirementType
suspensionDays: Numeric | null
threshold: Numeric | null
thresholdDays: Numeric | null
triggerType: TriggerType
}
export interface ComplianceTriggerSets {
id: string
name: string
}
export interface Coupons {
code: string
discount: number
id: string
softDeleted: Generated<boolean | null>
}
export interface CustomerCustomFieldPairs {
customerId: string
customFieldId: string
value: string
}
export interface CustomerExternalCompliance {
customerId: string
externalId: string
lastKnownStatus: ExternalComplianceStatus | null
lastUpdated: Generated<Timestamp>
service: string
}
export interface CustomerNotes {
content: Generated<string>
created: Generated<Timestamp>
customerId: string
id: string
lastEditedAt: Timestamp | null
lastEditedBy: string | null
title: Generated<string>
}
export interface Customers {
address: string | null
authorizedAt: Timestamp | null
authorizedOverride: Generated<VerificationType>
authorizedOverrideAt: Timestamp | null
authorizedOverrideBy: string | null
created: Generated<Timestamp>
email: string | null
emailAt: Timestamp | null
frontCameraAt: Timestamp | null
frontCameraOverride: Generated<VerificationType>
frontCameraOverrideAt: Timestamp | null
frontCameraOverrideBy: string | null
frontCameraPath: string | null
id: string
idCardData: { firstName: string; lastName: string }
idCardDataAt: Timestamp | null
idCardDataExpiration: Timestamp | null
idCardDataNumber: string | null
idCardDataOverride: Generated<VerificationType>
idCardDataOverrideAt: Timestamp | null
idCardDataOverrideBy: string | null
idCardDataRaw: string | null
idCardPhotoAt: Timestamp | null
idCardPhotoOverride: Generated<VerificationType>
idCardPhotoOverrideAt: Timestamp | null
idCardPhotoOverrideBy: string | null
idCardPhotoPath: string | null
isTestCustomer: Generated<boolean>
lastAuthAttempt: Timestamp | null
lastUsedMachine: string | null
name: string | null
phone: string | null
phoneAt: Timestamp | null
phoneOverride: Generated<VerificationType>
phoneOverrideAt: Timestamp | null
phoneOverrideBy: string | null
sanctions: boolean | null
sanctionsAt: Timestamp | null
sanctionsOverride: Generated<VerificationType>
sanctionsOverrideAt: Timestamp | null
sanctionsOverrideBy: string | null
smsOverride: Generated<VerificationType>
smsOverrideAt: Timestamp | null
smsOverrideBy: string | null
subscriberInfo: Json | null
subscriberInfoAt: Timestamp | null
subscriberInfoBy: string | null
suspendedUntil: Timestamp | null
usSsn: string | null
usSsnAt: Timestamp | null
usSsnOverride: Generated<VerificationType>
usSsnOverrideAt: Timestamp | null
usSsnOverrideBy: string | null
}
export interface CustomersCustomInfoRequests {
customerData: Json
customerId: string
infoRequestId: string
override: Generated<VerificationType>
overrideAt: Timestamp | null
overrideBy: string | null
}
export interface CustomFieldDefinitions {
active: Generated<boolean | null>
id: string
label: string
}
export interface CustomInfoRequests {
customRequest: Json | null
enabled: Generated<boolean>
id: string
}
export interface Devices {
cassette1: Generated<number>
cassette2: Generated<number>
cassette3: Generated<number>
cassette4: Generated<number>
created: Generated<Timestamp>
deviceId: string
diagnosticsFrontUpdatedAt: Timestamp | null
diagnosticsScanUpdatedAt: Timestamp | null
diagnosticsTimestamp: Timestamp | null
display: Generated<boolean>
lastOnline: Generated<Timestamp>
location: Generated<Json>
machineGroupId: Generated<string>
model: string | null
name: string
numberOfCassettes: Generated<number>
numberOfRecyclers: Generated<number>
paired: Generated<boolean>
recycler1: Generated<number>
recycler2: Generated<number>
recycler3: Generated<number>
recycler4: Generated<number>
recycler5: Generated<number>
recycler6: Generated<number>
restrictionLevel: Generated<number>
userConfigId: number | null
version: string | null
}
export interface EditedCustomerData {
created: Generated<Timestamp>
customerId: string
frontCameraAt: Timestamp | null
frontCameraBy: string | null
frontCameraPath: string | null
idCardData: { firstName: string; lastName: string }
idCardDataAt: Timestamp | null
idCardDataBy: string | null
idCardPhotoAt: Timestamp | null
idCardPhotoBy: string | null
idCardPhotoPath: string | null
name: string | null
nameAt: Timestamp | null
nameBy: string | null
subscriberInfo: Json | null
subscriberInfoAt: Timestamp | null
subscriberInfoBy: string | null
usSsn: string | null
usSsnAt: Timestamp | null
usSsnBy: string | null
}
export interface EmptyUnitBills {
cashboxBatchId: string | null
created: Generated<Timestamp>
deviceId: string
fiat: number
fiatCode: string
id: string
}
export interface HardwareCredentials {
created: Generated<Timestamp | null>
data: Json
id: string
lastUsed: Generated<Timestamp | null>
userId: string
}
export interface IndividualDiscounts {
customerId: string
discount: number
id: string
softDeleted: Generated<boolean | null>
}
export interface Logs {
deviceId: string | null
id: string
logLevel: string | null
message: string | null
serial: Generated<number>
serverTimestamp: Generated<Timestamp>
timestamp: Timestamp | null
}
export interface MachineEvents {
created: Generated<Timestamp>
deviceId: string
deviceTime: Timestamp | null
eventType: string
id: string
note: string | null
}
export interface MachineGroups {
complianceTriggerSetId: string | null
id: string
name: string
}
export interface MachineNetworkHeartbeat {
averagePacketLoss: Numeric
averageResponseTime: Numeric
created: Generated<Timestamp>
deviceId: string
id: string
}
export interface MachineNetworkPerformance {
created: Generated<Timestamp>
deviceId: string
downloadSpeed: Numeric
}
export interface MachinePings {
deviceId: string
deviceTime: Timestamp
updated: Generated<Timestamp>
}
export interface Migrations {
data: Json
id: Generated<number>
}
export interface Notifications {
created: Generated<Timestamp>
detail: Json | null
id: string
message: string
read: Generated<boolean>
type: NotificationType
valid: Generated<boolean>
}
export interface OperatorIds {
id: Generated<number>
operatorId: string
service: string
}
export interface PairingTokens {
created: Generated<Timestamp>
id: Generated<number>
name: string
token: string | null
}
export interface SanctionsLogs {
created: Generated<Timestamp>
customerId: string
deviceId: string
id: string
sanctionedAliasFullName: string
sanctionedAliasId: string | null
sanctionedId: string
}
export interface ServerLogs {
deviceId: string | null
id: string
logLevel: string | null
message: string | null
meta: Json | null
timestamp: Generated<Timestamp | null>
}
export interface SmsNotices {
allowToggle: Generated<boolean>
created: Generated<Timestamp>
enabled: Generated<boolean>
event: SmsNoticeEvent
id: string
message: string
messageName: string
}
export interface Trades {
created: Generated<Timestamp>
cryptoAtoms: Numeric
cryptoCode: string
error: string | null
fiatCode: string
id: Generated<number>
type: TradeType
}
export interface TransactionBatches {
closedAt: Timestamp | null
createdAt: Generated<Timestamp>
cryptoCode: string
errorMessage: string | null
id: string
status: Generated<TransactionBatchStatus>
}
export interface UnpairedDevices {
deviceId: string
id: string
model: string | null
name: string | null
paired: Timestamp
unpaired: Timestamp
}
export interface UserConfig {
created: Generated<Timestamp>
data: { accounts?: object; config?: object }
id: Generated<number>
schemaVersion: Generated<number>
type: string
valid: boolean
}
export interface UserRegisterTokens {
expire: Generated<Timestamp>
role: Generated<Role | null>
token: string
useFido: Generated<boolean | null>
username: string
}
export interface Users {
created: Generated<Timestamp>
enabled: Generated<boolean | null>
id: string
lastAccessed: Generated<Timestamp>
lastAccessedAddress: string | null
lastAccessedFrom: string | null
password: string | null
role: Generated<Role>
tempTwofaCode: string | null
twofaCode: string | null
username: string
}
export interface UserSessions {
expire: Timestamp
sess: Json
sid: string
}
export interface DB {
authTokens: AuthTokens
bills: Bills
blacklist: Blacklist
blacklistMessages: BlacklistMessages
cashInActions: CashInActions
cashInTxs: CashInTxs
cashinTxTrades: CashinTxTrades
cashOutActions: CashOutActions
cashOutTxs: CashOutTxs
cashoutTxTrades: CashoutTxTrades
cashUnitOperation: CashUnitOperation
complianceOverrides: ComplianceOverrides
complianceTriggers: ComplianceTriggers
complianceTriggerSets: ComplianceTriggerSets
coupons: Coupons
customerCustomFieldPairs: CustomerCustomFieldPairs
customerExternalCompliance: CustomerExternalCompliance
customerNotes: CustomerNotes
customers: Customers
customersCustomInfoRequests: CustomersCustomInfoRequests
customFieldDefinitions: CustomFieldDefinitions
customInfoRequests: CustomInfoRequests
devices: Devices
editedCustomerData: EditedCustomerData
emptyUnitBills: EmptyUnitBills
hardwareCredentials: HardwareCredentials
individualDiscounts: IndividualDiscounts
logs: Logs
machineEvents: MachineEvents
machineGroups: MachineGroups
machineNetworkHeartbeat: MachineNetworkHeartbeat
machineNetworkPerformance: MachineNetworkPerformance
machinePings: MachinePings
migrations: Migrations
notifications: Notifications
operatorIds: OperatorIds
pairingTokens: PairingTokens
sanctionsLogs: SanctionsLogs
serverLogs: ServerLogs
smsNotices: SmsNotices
trades: Trades
transactionBatches: TransactionBatches
unpairedDevices: UnpairedDevices
userConfig: UserConfig
userRegisterTokens: UserRegisterTokens
users: Users
userSessions: UserSessions
}

View file

@ -0,0 +1,114 @@
import type { Json } from './types/types.js'
import type { DBOrTx } from './db.js'
import { inTransaction } from './db.js'
import { notifyReload } from './notify.js'
const NEW_SETTINGS_LOADER_SCHEMA_VERSION = 2
function getRow(dbOrTx: DBOrTx, type: 'accounts' | 'config', version?: number) {
let query = dbOrTx
.selectFrom('userConfig as uc')
.select(['uc.id', 'uc.data'])
.where('uc.type', '=', type)
.where('uc.schemaVersion', '=', NEW_SETTINGS_LOADER_SCHEMA_VERSION)
.where('uc.valid', '=', true)
.orderBy('uc.id', 'desc')
.limit(1)
if (version) query = query.where('uc.id', '=', version)
return query
}
export function insertConfigRow(dbOrTx: DBOrTx, config: object) {
return dbOrTx
.insertInto('userConfig')
.values({
type: 'config',
data: config,
valid: true,
schemaVersion: NEW_SETTINGS_LOADER_SCHEMA_VERSION,
})
.execute()
}
function _loadConfigWithVersion(dbOrTx: DBOrTx, version?: number) {
return getRow(dbOrTx, 'config', version)
.executeTakeFirstOrThrow()
.then(row => ({
config: row?.data?.config ?? {},
version: row?.id,
}))
}
export function loadAccounts(dbOrTx: DBOrTx) {
return getRow(dbOrTx, 'accounts')
.executeTakeFirstOrThrow()
.then(row => row?.data?.accounts ?? {})
}
export function loadConfig(dbOrTx: DBOrTx) {
return _loadConfigWithVersion(dbOrTx).then(({ config }) => config)
}
export async function load(dbOrTx: DBOrTx, version?: number) {
const config = await _loadConfigWithVersion(dbOrTx, version)
const accounts = await loadAccounts(dbOrTx)
return {
config: config.config,
accounts,
version: config.version,
}
}
function updateAccounts(dbOrTx: DBOrTx, accounts: object) {
return dbOrTx
.updateTable('userConfig')
.set({
data: accounts,
valid: true,
schemaVersion: NEW_SETTINGS_LOADER_SCHEMA_VERSION,
})
.where('type', '=', 'accounts')
.execute()
}
function insertAccounts(dbOrTx: DBOrTx, accounts: Json) {
return dbOrTx
.insertInto('userConfig')
.columns(['type', 'data', 'valid', 'schemaVersion'])
.expression(eb =>
eb
.selectFrom('userConfig as uc')
.select([
eb.val('accounts').as('type'),
eb.val(accounts).as('data'),
eb.val(true).as('valid'),
eb.val(NEW_SETTINGS_LOADER_SCHEMA_VERSION).as('schemaVersion'),
])
.where(({ not, exists, selectFrom }) =>
not(
exists(
selectFrom('userConfig as uc')
.select('uc.type')
.where('uc.type', '=', 'accounts'),
),
),
),
)
.execute()
}
export function saveAccounts(
dbOrTx: DBOrTx,
mergeAccounts: (old: object) => Json,
) {
return inTransaction(async tx => {
const currentAccounts = await loadAccounts(tx)
const newAccounts = mergeAccounts(currentAccounts)
await updateAccounts(tx, { accounts: newAccounts })
await insertAccounts(tx, { accounts: newAccounts })
await notifyReload(tx)
return newAccounts
}, dbOrTx)
}

View file

@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Node 22",
"_version": "22.0.0",
"compilerOptions": {
"lib": ["es2023"],
"types": ["node"],
"module": "nodenext",
"target": "es2022",
"allowJs": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "node16",
"noEmit": false,
"outDir": "./lib",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["lib", "node_modules"]
}