v12.0.0 - initial commit
This commit is contained in:
commit
e2c49ea43c
1145 changed files with 97211 additions and 0 deletions
39
packages/typesafe-db/package.json
Normal file
39
packages/typesafe-db/package.json
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
125
packages/typesafe-db/src/compliance-triggers.ts
Normal file
125
packages/typesafe-db/src/compliance-triggers.ts
Normal 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()
|
||||
}
|
||||
216
packages/typesafe-db/src/customers.ts
Normal file
216
packages/typesafe-db/src/customers.ts
Normal 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 }
|
||||
42
packages/typesafe-db/src/db.ts
Normal file
42
packages/typesafe-db/src/db.ts
Normal 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)
|
||||
}
|
||||
9
packages/typesafe-db/src/index.ts
Normal file
9
packages/typesafe-db/src/index.ts
Normal 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'
|
||||
30
packages/typesafe-db/src/interpolated-query-logger.ts
Normal file
30
packages/typesafe-db/src/interpolated-query-logger.ts
Normal 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
|
||||
}
|
||||
64
packages/typesafe-db/src/machine-groups.ts
Normal file
64
packages/typesafe-db/src/machine-groups.ts
Normal 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()
|
||||
}
|
||||
35
packages/typesafe-db/src/machines.ts
Normal file
35
packages/typesafe-db/src/machines.ts
Normal 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
|
||||
}
|
||||
24
packages/typesafe-db/src/notify.ts
Normal file
24
packages/typesafe-db/src/notify.ts
Normal 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')
|
||||
}
|
||||
4
packages/typesafe-db/src/pg-error-codes.ts
Normal file
4
packages/typesafe-db/src/pg-error-codes.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export const PG_ERROR_CODES = {
|
||||
UNIQUE_VIOLATION: '23505',
|
||||
FOREIGN_KEY_VIOLATION: '23503',
|
||||
} as const
|
||||
389
packages/typesafe-db/src/transactions.ts
Normal file
389
packages/typesafe-db/src/transactions.ts
Normal 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,
|
||||
}
|
||||
35
packages/typesafe-db/src/types/manual.types.d.ts
vendored
Normal file
35
packages/typesafe-db/src/types/manual.types.d.ts
vendored
Normal 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>
|
||||
796
packages/typesafe-db/src/types/types.d.ts
vendored
Normal file
796
packages/typesafe-db/src/types/types.d.ts
vendored
Normal 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
|
||||
}
|
||||
114
packages/typesafe-db/src/user-config.ts
Normal file
114
packages/typesafe-db/src/user-config.ts
Normal 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)
|
||||
}
|
||||
23
packages/typesafe-db/tsconfig.json
Normal file
23
packages/typesafe-db/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue