From 64e358f61c2c55d46abefd3ebcd2f8c9983f39e6 Mon Sep 17 00:00:00 2001 From: Rafael Taranto Date: Thu, 22 May 2025 11:47:50 +0100 Subject: [PATCH] feat: typesafe query and more UI changes --- .gitignore | 1 + package-lock.json | 305 ++++++- package.json | 3 +- .../src/pages/Customers/CustomerProfile.jsx | 1 - .../src/pages/Customers/CustomersList.jsx | 58 +- .../src/pages/Customers/helper.test.js | 27 + packages/server/lib/customers.js | 86 +- .../graphql/resolvers/customer.resolver.js | 3 +- packages/typesafe-db/package.json | 20 + packages/typesafe-db/src/customers.ts | 159 ++++ packages/typesafe-db/src/db.ts | 24 + packages/typesafe-db/src/types/types.d.ts | 745 ++++++++++++++++++ packages/typesafe-db/tsconfig.json | 24 + 13 files changed, 1347 insertions(+), 109 deletions(-) create mode 100644 packages/typesafe-db/package.json create mode 100644 packages/typesafe-db/src/customers.ts create mode 100644 packages/typesafe-db/src/db.ts create mode 100644 packages/typesafe-db/src/types/types.d.ts create mode 100644 packages/typesafe-db/tsconfig.json diff --git a/.gitignore b/.gitignore index 8bf015e1..ad48bf9c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ packages/server/certs/ packages/server/tests/stress/machines packages/server/tests/stress/config.json +packages/typesafe-db/lib/ diff --git a/package-lock.json b/package-lock.json index 380af821..d9ab6abe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "./LICENSE", "workspaces": [ "packages/server", - "packages/admin-ui" + "packages/admin-ui", + "packages/typesafe-db" ], "devDependencies": { "@eslint/css": "^0.7.0", @@ -10688,6 +10689,16 @@ "wrappy": "1" } }, + "node_modules/diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", @@ -10844,6 +10855,22 @@ "url": "https://dotenvx.com" } }, + "node_modules/dotenv-expand": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.2.tgz", + "integrity": "sha512-lXpXz2ZE1cea1gL4sz2Ipj8y4PiVjytYr3Ij0SWoms1PGxIv7m2CRKuRuCRtHdVuvM/hNJPMxt5PbhboNC4dPQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/downshift": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/downshift/-/downshift-9.0.9.tgz", @@ -11054,6 +11081,16 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -13465,6 +13502,101 @@ "assert-plus": "^1.0.0" } }, + "node_modules/git-diff": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/git-diff/-/git-diff-2.0.6.tgz", + "integrity": "sha512-/Iu4prUrydE3Pb3lCBMbcSNIf81tgGt0W1ZwknnyF62t3tHmtiJTRj0f+1ZIhp3+Rh0ktz1pJVoa7ZXUCskivA==", + "dev": true, + "license": "ISC", + "dependencies": { + "chalk": "^2.3.2", + "diff": "^3.5.0", + "loglevel": "^1.6.1", + "shelljs": "^0.8.1", + "shelljs.exec": "^1.1.7" + }, + "engines": { + "node": ">= 4.8.0" + } + }, + "node_modules/git-diff/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-diff/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-diff/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/git-diff/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/git-diff/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/git-diff/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/git-diff/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -14629,6 +14761,16 @@ "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==" }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/io-ts": { "version": "2.2.20", "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.20.tgz", @@ -16941,6 +17083,120 @@ "node": ">=6" } }, + "node_modules/kysely": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.2.tgz", + "integrity": "sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/kysely-codegen": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/kysely-codegen/-/kysely-codegen-0.18.5.tgz", + "integrity": "sha512-bj6DMsXcKo0PrrXUk/fdjFgNC6Pwq+HPBCqhNGuD57gwUJZdci2s2OqhNneQeYpAIWGot7/481WdzTyXrClY2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "cosmiconfig": "^9.0.0", + "dotenv": "^16.5.0", + "dotenv-expand": "^12.0.2", + "git-diff": "^2.0.6", + "micromatch": "^4.0.8", + "minimist": "^1.2.8", + "pluralize": "^8.0.0", + "zod": "^3.24.4" + }, + "bin": { + "kysely-codegen": "dist/cli/bin.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@libsql/kysely-libsql": ">=0.3.0 <0.5.0", + "@tediousjs/connection-string": ">=0.5.0 <0.6.0", + "better-sqlite3": ">=7.6.2 <8.0.0", + "kysely": ">=0.27.0 <1.0.0", + "kysely-bun-sqlite": ">=0.3.2 <1.0.0", + "kysely-bun-worker": ">=1.2.0 <2.0.0", + "mysql2": ">=2.3.3 <4.0.0", + "pg": ">=8.8.0 <9.0.0", + "tarn": ">=3.0.0 <4.0.0", + "tedious": ">=18.0.0 <20.0.0" + }, + "peerDependenciesMeta": { + "@libsql/kysely-libsql": { + "optional": true + }, + "@tediousjs/connection-string": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "kysely": { + "optional": false + }, + "kysely-bun-sqlite": { + "optional": true + }, + "kysely-bun-worker": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "tarn": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/kysely-codegen/node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/kysely-codegen/node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/lamassu-admin": { "resolved": "packages/admin-ui", "link": true @@ -21212,6 +21468,18 @@ "node": ">=8.10.0" } }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/referrer-policy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.2.0.tgz", @@ -22888,6 +23156,34 @@ "node": ">=8" } }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shelljs.exec": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/shelljs.exec/-/shelljs.exec-1.1.8.tgz", + "integrity": "sha512-vFILCw+lzUtiwBAHV8/Ex8JsFjelFMdhONIsgKNLgTzeRckp2AOYRQtHJE/9LhNvdMmE27AGtzWx0+DHpwIwSw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/shellwords": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", @@ -25966,12 +26262,15 @@ "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" }, + "node_modules/typesafe-db": { + "resolved": "packages/typesafe-db", + "link": true + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -28778,9 +29077,7 @@ } }, "packages/typesafe-db": { - "name": "lamassu-typesafe-db", "version": "11.0.0-beta.0", - "extraneous": true, "license": "../LICENSE", "dependencies": { "kysely": "^0.28.2", diff --git a/package.json b/package.json index 78242888..4808b5ea 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ }, "workspaces": [ "packages/server", - "packages/admin-ui" + "packages/admin-ui", + "packages/typesafe-db" ], "devDependencies": { "@eslint/css": "^0.7.0", diff --git a/packages/admin-ui/src/pages/Customers/CustomerProfile.jsx b/packages/admin-ui/src/pages/Customers/CustomerProfile.jsx index 0b7f1e2c..5a102a3b 100644 --- a/packages/admin-ui/src/pages/Customers/CustomerProfile.jsx +++ b/packages/admin-ui/src/pages/Customers/CustomerProfile.jsx @@ -290,7 +290,6 @@ const CustomerProfile = memo(() => { const [error, setError] = useState(null) const [clickedItem, setClickedItem] = useState('overview') const { id: customerId } = useParams() - console.log(customerId) const { data: customerResponse, diff --git a/packages/admin-ui/src/pages/Customers/CustomersList.jsx b/packages/admin-ui/src/pages/Customers/CustomersList.jsx index 72530f04..9258e039 100644 --- a/packages/admin-ui/src/pages/Customers/CustomersList.jsx +++ b/packages/admin-ui/src/pages/Customers/CustomersList.jsx @@ -1,3 +1,6 @@ +import IconButton from '@mui/material/IconButton' +import Tooltip from '@mui/material/Tooltip' +import Visibility from '@mui/icons-material/Visibility' import { format } from 'date-fns/fp' import * as R from 'ramda' import React, { useMemo } from 'react' @@ -40,7 +43,6 @@ const CustomersList = ({ data, country, onClick, loading }) => { ...alignRight, }, { - id: 'totalSpent', accessorKey: 'totalSpent', size: 152, enableColumnFilter: false, @@ -49,17 +51,6 @@ const CustomersList = ({ data, country, onClick, loading }) => { header: 'Total spent', ...alignRight, }, - { - header: 'Last active', - // accessorKey: 'lastActive', - accessorFn: it => new Date(it.lastActive), - size: 133, - enableColumnFilter: false, - Cell: ({ cell }) => - (cell.getValue() && - format('yyyy-MM-dd', new Date(cell.getValue()))) ?? - '', - }, { header: 'Last transaction', ...alignRight, @@ -80,12 +71,34 @@ const CustomersList = ({ data, country, onClick, loading }) => { ) }, }, + { + accessorKey: 'lastActive', + header: 'Last active', + size: 133, + enableColumnFilter: false, + Cell: ({ cell }) => + (cell.getValue() && + format('yyyy-MM-dd', new Date(cell.getValue()))) ?? + '', + }, { header: 'Status', - id: 'status', - size: 100, + size: 150, enableColumnFilter: false, accessorKey: 'authorizedStatus', + sortingFn: (rowA, rowB) => { + const statusOrder = { success: 0, warning: 1, error: 2 } + const statusA = rowA.original.authorizedStatus.type + const statusB = rowB.original.authorizedStatus.type + + if (statusA === statusB) { + return rowA.original.authorizedStatus.label.localeCompare( + rowB.original.authorizedStatus.label, + ) + } + + return statusOrder[statusA] - statusOrder[statusB] + }, Cell: ({ cell }) => , }, ], @@ -101,13 +114,22 @@ const CustomersList = ({ data, country, onClick, loading }) => { columnVisibility: { id: false, }, + sorting: [{ id: 'lastActive', desc: true }], + columnPinning: { right: ['mrt-row-actions'] }, }, state: { isLoading: loading }, getRowId: it => it.id, - muiTableBodyRowProps: ({ row }) => ({ - onClick: () => onClick(row), - sx: { cursor: 'pointer' }, - }), + enableRowActions: true, + positionActionsColumn: 'last', + renderRowActions: ({ row }) => ( +
+ + onClick(row)}> + + + +
+ ), }) return ( diff --git a/packages/admin-ui/src/pages/Customers/helper.test.js b/packages/admin-ui/src/pages/Customers/helper.test.js index a3180eff..b8836d79 100644 --- a/packages/admin-ui/src/pages/Customers/helper.test.js +++ b/packages/admin-ui/src/pages/Customers/helper.test.js @@ -293,4 +293,31 @@ describe('getAuthorizedStatus', () => { type: 'error', }) }) + + it('should return rejected status for blocked custom info request', () => { + const customer = { + authorizedOverride: null, + isSuspended: false, + customInfoRequests: [ + { + infoRequestId: '550e8400-e29b-41d4-a716-446655440000', + override: 'blocked', + }, + ], + } + + const triggers = { + automation: 'manual', + overrides: [], + } + + const customRequests = [{ id: '550e8400-e29b-41d4-a716-446655440000' }] + + const result = getAuthorizedStatus(customer, triggers, customRequests) + + expect(result).toEqual({ + label: 'Rejected', + type: 'error', + }) + }) }) diff --git a/packages/server/lib/customers.js b/packages/server/lib/customers.js index 67e6d806..dec3e7d6 100644 --- a/packages/server/lib/customers.js +++ b/packages/server/lib/customers.js @@ -8,7 +8,6 @@ const fs = require('fs') const util = require('util') const db = require('./db') -const anonymous = require('../lib/constants').anonymousCustomer const complianceOverrides = require('./compliance_overrides') const writeFile = util.promisify(fs.writeFile) const notifierQueries = require('./notifier/queries') @@ -17,6 +16,7 @@ const sms = require('./sms') const settingsLoader = require('./new-settings-loader') const logger = require('./logger') const externalCompliance = require('./compliance-external') +const { getCustomerList } = require('typesafe-db/lib/customers') const { APPROVED, RETRY } = require('./plugins/compliance/consts') @@ -489,88 +489,8 @@ function getSlimCustomerByIdBatch(ids) { return db.any(sql, [ids]).then(customers => _.map(camelize, customers)) } -// TODO: getCustomersList and getCustomerById are very similar, so this should be refactored - -/** - * Query all customers, ordered by last activity - * and with aggregate columns based on their - * transactions - * - * @returns {array} Array of customers with it's transactions aggregations - */ - -function getCustomersList( - phone = null, - name = null, - address = null, - id = null, - email = null, -) { - const passableErrorCodes = _.map( - Pgp.as.text, - TX_PASSTHROUGH_ERROR_CODES, - ).join(',') - - const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_override, - phone, email, sms_override, id_card_data, id_card_data_override, id_card_data_expiration, - id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at, - sanctions_override, total_txs, total_spent, GREATEST(created, last_transaction, last_data_provided, last_auth_attempt) AS last_active, fiat AS last_tx_fiat, - fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, custom_fields, notes, is_test_customer - FROM ( - SELECT c.id, c.authorized_override, - greatest(0, date_part('day', c.suspended_until - NOW())) AS days_suspended, - c.suspended_until > NOW() AS is_suspended, - c.front_camera_path, c.front_camera_override, - c.phone, c.email, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration, - c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions, c.last_auth_attempt, - GREATEST(c.phone_at, c.email_at, c.id_card_data_at, c.front_camera_at, c.id_card_photo_at, c.us_ssn_at) AS last_data_provided, - c.sanctions_at, c.sanctions_override, c.is_test_customer, c.created, t.tx_class, t.fiat, t.fiat_code, t.created as last_transaction, cn.notes, - row_number() OVER (partition by c.id order by t.created desc) AS rn, - sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (partition by c.id) AS total_txs, - coalesce(sum(CASE WHEN error_code IS NULL OR error_code NOT IN ($1^) THEN t.fiat ELSE 0 END) OVER (partition by c.id), 0) AS total_spent, ccf.custom_fields - FROM customers c LEFT OUTER JOIN ( - SELECT 'cashIn' AS tx_class, id, fiat, fiat_code, created, customer_id, error_code - FROM cash_in_txs WHERE send_confirmed = true OR batched = true UNION - SELECT 'cashOut' AS tx_class, id, fiat, fiat_code, created, customer_id, error_code - FROM cash_out_txs WHERE confirmed_at IS NOT NULL) AS t ON c.id = t.customer_id - LEFT OUTER JOIN ( - SELECT cf.customer_id, json_agg(json_build_object('id', cf.custom_field_id, 'label', cf.label, 'value', cf.value)) AS custom_fields FROM ( - SELECT ccfp.custom_field_id, ccfp.customer_id, cfd.label, ccfp.value FROM custom_field_definitions cfd - LEFT OUTER JOIN customer_custom_field_pairs ccfp ON cfd.id = ccfp.custom_field_id - ) cf GROUP BY cf.customer_id - ) ccf ON c.id = ccf.customer_id - LEFT OUTER JOIN ( - SELECT customer_id, coalesce(json_agg(customer_notes.*), '[]'::json) AS notes FROM customer_notes - GROUP BY customer_notes.customer_id - ) cn ON c.id = cn.customer_id - WHERE c.id != $2 - ) AS cl WHERE rn = 1 - AND ($4 IS NULL OR phone = $4) - AND ($5 IS NULL OR CONCAT(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') = $5 OR id_card_data::json->>'firstName' = $5 OR id_card_data::json->>'lastName' = $5) - AND ($6 IS NULL OR id_card_data::json->>'address' = $6) - AND ($7 IS NULL OR id_card_data::json->>'documentNumber' = $7) - AND ($8 IS NULL OR email = $8) - ORDER BY last_active DESC - limit $3` - return db - .any(sql, [ - passableErrorCodes, - anonymous.uuid, - null, - phone, - name, - address, - id, - email, - ]) - .then(customers => - Promise.all( - _.map( - customer => getCustomInfoRequestsData(customer).then(camelizeDeep), - customers, - ), - ), - ) +function getCustomersList() { + return getCustomerList({ withCustomInfoRequest: true }) } /** diff --git a/packages/server/lib/new-admin/graphql/resolvers/customer.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/customer.resolver.js index 9dbf2951..c574b272 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/customer.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/customer.resolver.js @@ -17,8 +17,7 @@ const resolvers = { isAnonymous: parent => parent.customerId === anonymous.uuid, }, Query: { - customers: (...[, { phone, email, name, address, id }]) => - customers.getCustomersList(phone, name, address, id, email), + customers: () => customers.getCustomersList(), customer: (...[, { customerId }]) => customers.getCustomerById(customerId).then(addLastUsedMachineName), }, diff --git a/packages/typesafe-db/package.json b/packages/typesafe-db/package.json new file mode 100644 index 00000000..0423804b --- /dev/null +++ b/packages/typesafe-db/package.json @@ -0,0 +1,20 @@ +{ + "name": "typesafe-db", + "version": "11.0.0-beta.0", + "license": "../LICENSE", + "type": "module", + "devDependencies": { + "kysely-codegen": "^0.18.5", + "typescript": "^5.8.3" + }, + "scripts": { + "build": "tsc", + "start": "tsc --watch", + "clean": "rm -rf lib", + "generate-types": "kysely-codegen --camel-case --out-file ./src/types/types.d.ts" + }, + "dependencies": { + "kysely": "^0.28.2", + "pg": "^8.16.0" + } +} diff --git a/packages/typesafe-db/src/customers.ts b/packages/typesafe-db/src/customers.ts new file mode 100644 index 00000000..d56f2790 --- /dev/null +++ b/packages/typesafe-db/src/customers.ts @@ -0,0 +1,159 @@ +import db from './db.js' +import { ExpressionBuilder, sql } from 'kysely' +import { Customers, DB } from './types/types.js' +import { jsonObjectFrom } from 'kysely/helpers/postgres' + +type CustomerEB = ExpressionBuilder + +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('c.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('c.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') +} + +interface GetCustomerListOptions { + withCustomInfoRequest: boolean +} + +const defaultOptions: GetCustomerListOptions = { + withCustomInfoRequest: false, +} + +// TODO left join lateral is having issues deriving type +function getCustomerList( + options: GetCustomerListOptions = defaultOptions, +): Promise { + return db + .selectFrom('customers as c') + .leftJoinLateral(joinTxsTotals, join => join.onTrue()) + .leftJoinLateral(joinLatestTx, join => join.onTrue()) + .select(({ eb, fn, val, ref }) => [ + 'id', + 'authorizedOverride', + 'frontCameraPath', + 'frontCameraOverride', + 'idCardPhotoPath', + 'idCardPhotoOverride', + 'idCardData', + 'idCardDataOverride', + 'email', + 'usSsn', + 'usSsnOverride', + 'sanctions', + 'sanctionsOverride', + 'txStats.totalSpent', + 'txStats.totalTxs', + ref('lastTx.fiatCode').as('lastTxFiatCode'), + ref('lastTx.fiat').as('lastTxFiat'), + ref('lastTx.txClass').as('lastTxClass'), + fn('GREATEST', [ + 'c.created', + // 'lastTx.created', + 'c.phoneAt', + 'c.emailAt', + 'c.idCardDataAt', + 'c.frontCameraAt', + 'c.idCardPhotoAt', + 'c.usSsnAt', + 'c.lastAuthAttempt', + ]).as('lastActive'), + eb('c.suspendedUntil', '>', fn('NOW', [])).as('isSuspended'), + fn('GREATEST', [ + val(0), + fn('date_part', [ + val('day'), + eb('c.suspendedUntil', '-', fn('NOW', [])), + ]), + ]).as('daysSuspended'), + ]) + .$if(options.withCustomInfoRequest, qb => + qb.select(({ eb, ref }) => + jsonObjectFrom( + eb + .selectFrom('customersCustomInfoRequests') + .selectAll() + .where('customerId', '=', ref('c.id')), + ).as('customInfoRequestData'), + ), + ) + .orderBy('lastActive', 'desc') + .execute() +} + +export { getCustomerList } diff --git a/packages/typesafe-db/src/db.ts b/packages/typesafe-db/src/db.ts new file mode 100644 index 00000000..959d6161 --- /dev/null +++ b/packages/typesafe-db/src/db.ts @@ -0,0 +1,24 @@ +import { DB } from './types/types.js' +import { Pool } from 'pg' +import { Kysely, PostgresDialect, CamelCasePlugin } from 'kysely' +import { PSQL_URL } from 'lamassu-server/lib/constants.js' + +const dialect = new PostgresDialect({ + pool: new Pool({ + connectionString: PSQL_URL, + max: 5, + }), +}) + +export default new Kysely({ + dialect, + plugins: [new CamelCasePlugin()], + log(event) { + if (event.level === 'query') { + console.log('Query:', event.query.sql) + console.log('Parameters:', event.query.parameters) + console.log('Duration:', event.queryDurationMillis + 'ms') + console.log('---') + } + }, +}) diff --git a/packages/typesafe-db/src/types/types.d.ts b/packages/typesafe-db/src/types/types.d.ts new file mode 100644 index 00000000..caaf0434 --- /dev/null +++ b/packages/typesafe-db/src/types/types.d.ts @@ -0,0 +1,745 @@ +/** + * 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 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 extends ColumnType + ? ColumnType + : ColumnType + +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 + +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 + +export type TradeType = 'buy' | 'sell' + +export type TransactionBatchStatus = 'failed' | 'open' | 'ready' | 'sent' + +export type VerificationType = 'automatic' | 'blocked' | 'verified' + +export interface AuthTokens { + expire: Generated + token: string + type: AuthTokenType + userId: string | null +} + +export interface Bills { + cashboxBatchId: string | null + cashInFee: Numeric + cashInTxsId: string + created: Generated + cryptoCode: Generated + destinationUnit: Generated + deviceTime: Int8 + fiat: number + fiatCode: string + id: string + legacy: Generated +} + +export interface Blacklist { + address: string + blacklistMessageId: Generated +} + +export interface BlacklistMessages { + allowToggle: Generated + content: string + id: string + label: string +} + +export interface CashInActions { + action: string + created: Generated + error: string | null + errorCode: string | null + id: Generated + txHash: string | null + txId: string +} + +export interface CashInTxs { + batched: Generated + batchId: string | null + batchTime: Timestamp | null + cashInFee: Numeric + commissionPercentage: Generated + created: Generated + cryptoAtoms: Numeric + cryptoCode: string + customerId: Generated + 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 + minimumTx: number + operatorCompleted: Generated + phone: string | null + rawTickerPrice: Generated + send: Generated + sendConfirmed: Generated + sendPending: Generated + sendTime: Timestamp | null + termsAccepted: Generated + timedout: Generated + toAddress: string + txCustomerPhotoAt: Timestamp | null + txCustomerPhotoPath: string | null + txHash: string | null + txVersion: number + walletScore: number | null +} + +export interface CashinTxTrades { + tradeId: Generated + txId: string +} + +export interface CashOutActions { + action: string + created: Generated + 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 + 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 + 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 + 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 + confirmedAt: Timestamp | null + created: Generated + cryptoAtoms: Numeric + cryptoCode: string + customerId: Generated + 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 + dispenseConfirmed: Generated + email: string | null + error: string | null + errorCode: string | null + fiat: Numeric + fiatCode: string + fixedFee: Generated + hdIndex: Generated + id: string + layer2Address: string | null + notified: Generated + 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 + receivedCryptoAtoms: Generated + redeem: Generated + status: Generated + swept: Generated + termsAccepted: Generated + timedout: Generated + toAddress: string + txCustomerPhotoAt: Timestamp | null + txCustomerPhotoPath: string | null + txVersion: number + walletScore: number | null +} + +export interface CashoutTxTrades { + tradeId: Generated + txId: string +} + +export interface CashUnitOperation { + billCountOverride: number | null + created: Generated + 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 Coupons { + code: string + discount: number + id: string + softDeleted: Generated +} + +export interface CustomerCustomFieldPairs { + customerId: string + customFieldId: string + value: string +} + +export interface CustomerExternalCompliance { + customerId: string + externalId: string + lastKnownStatus: ExternalComplianceStatus | null + lastUpdated: Generated + service: string +} + +export interface CustomerNotes { + content: Generated + created: Generated + customerId: string + id: string + lastEditedAt: Timestamp | null + lastEditedBy: string | null + title: Generated +} + +export interface Customers { + address: string | null + authorizedAt: Timestamp | null + authorizedOverride: Generated + authorizedOverrideAt: Timestamp | null + authorizedOverrideBy: string | null + created: Generated + email: string | null + emailAt: Timestamp | null + frontCameraAt: Timestamp | null + frontCameraOverride: Generated + frontCameraOverrideAt: Timestamp | null + frontCameraOverrideBy: string | null + frontCameraPath: string | null + id: string + idCardData: Json | null + idCardDataAt: Timestamp | null + idCardDataExpiration: Timestamp | null + idCardDataNumber: string | null + idCardDataOverride: Generated + idCardDataOverrideAt: Timestamp | null + idCardDataOverrideBy: string | null + idCardDataRaw: string | null + idCardPhotoAt: Timestamp | null + idCardPhotoOverride: Generated + idCardPhotoOverrideAt: Timestamp | null + idCardPhotoOverrideBy: string | null + idCardPhotoPath: string | null + isTestCustomer: Generated + lastAuthAttempt: Timestamp | null + lastUsedMachine: string | null + name: string | null + phone: string | null + phoneAt: Timestamp | null + phoneOverride: Generated + phoneOverrideAt: Timestamp | null + phoneOverrideBy: string | null + sanctions: boolean | null + sanctionsAt: Timestamp | null + sanctionsOverride: Generated + sanctionsOverrideAt: Timestamp | null + sanctionsOverrideBy: string | null + smsOverride: Generated + 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 + usSsnOverrideAt: Timestamp | null + usSsnOverrideBy: string | null +} + +export interface CustomersCustomInfoRequests { + customerData: Json + customerId: string + infoRequestId: string + override: Generated + overrideAt: Timestamp | null + overrideBy: string | null +} + +export interface CustomFieldDefinitions { + active: Generated + id: string + label: string +} + +export interface CustomInfoRequests { + customRequest: Json | null + enabled: Generated + id: string +} + +export interface Devices { + cassette1: Generated + cassette2: Generated + cassette3: Generated + cassette4: Generated + created: Generated + deviceId: string + diagnosticsFrontUpdatedAt: Timestamp | null + diagnosticsScanUpdatedAt: Timestamp | null + diagnosticsTimestamp: Timestamp | null + display: Generated + lastOnline: Generated + location: Generated + model: string | null + name: string + numberOfCassettes: Generated + numberOfRecyclers: Generated + paired: Generated + recycler1: Generated + recycler2: Generated + recycler3: Generated + recycler4: Generated + recycler5: Generated + recycler6: Generated + userConfigId: number | null + version: string | null +} + +export interface EditedCustomerData { + created: Generated + customerId: string + frontCameraAt: Timestamp | null + frontCameraBy: string | null + frontCameraPath: string | null + idCardData: Json | null + 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 + deviceId: string + fiat: number + fiatCode: string + id: string +} + +export interface HardwareCredentials { + created: Generated + data: Json + id: string + lastUsed: Generated + userId: string +} + +export interface IndividualDiscounts { + customerId: string + discount: number + id: string + softDeleted: Generated +} + +export interface Logs { + deviceId: string | null + id: string + logLevel: string | null + message: string | null + serial: Generated + serverTimestamp: Generated + timestamp: Timestamp | null +} + +export interface MachineEvents { + created: Generated + deviceId: string + deviceTime: Timestamp | null + eventType: string + id: string + note: string | null +} + +export interface MachineNetworkHeartbeat { + averagePacketLoss: Numeric + averageResponseTime: Numeric + created: Generated + deviceId: string + id: string +} + +export interface MachineNetworkPerformance { + created: Generated + deviceId: string + downloadSpeed: Numeric +} + +export interface MachinePings { + deviceId: string + deviceTime: Timestamp + updated: Generated +} + +export interface Migrations { + data: Json + id: Generated +} + +export interface Notifications { + created: Generated + detail: Json | null + id: string + message: string + read: Generated + type: NotificationType + valid: Generated +} + +export interface OperatorIds { + id: Generated + operatorId: string + service: string +} + +export interface PairingTokens { + created: Generated + id: Generated + name: string + token: string | null +} + +export interface SanctionsLogs { + created: Generated + 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 +} + +export interface SmsNotices { + allowToggle: Generated + created: Generated + enabled: Generated + event: SmsNoticeEvent + id: string + message: string + messageName: string +} + +export interface Trades { + created: Generated + cryptoAtoms: Numeric + cryptoCode: string + error: string | null + fiatCode: string + id: Generated + type: TradeType +} + +export interface TransactionBatches { + closedAt: Timestamp | null + createdAt: Generated + cryptoCode: string + errorMessage: string | null + id: string + status: Generated +} + +export interface UnpairedDevices { + deviceId: string + id: string + model: string | null + name: string | null + paired: Timestamp + unpaired: Timestamp +} + +export interface UserConfig { + created: Generated + data: Json + id: Generated + schemaVersion: Generated + type: string + valid: boolean +} + +export interface UserRegisterTokens { + expire: Generated + role: Generated + token: string + useFido: Generated + username: string +} + +export interface Users { + created: Generated + enabled: Generated + id: string + lastAccessed: Generated + lastAccessedAddress: string | null + lastAccessedFrom: string | null + password: string | null + role: Generated + 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 + 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 + 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 +} diff --git a/packages/typesafe-db/tsconfig.json b/packages/typesafe-db/tsconfig.json new file mode 100644 index 00000000..aa077c9f --- /dev/null +++ b/packages/typesafe-db/tsconfig.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Node 22", + "_version": "22.0.0", + + "compilerOptions": { + "lib": ["es2023"], + "module": "nodenext", + "target": "es2022", + + "allowJs": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "node16", + + "outDir": "./lib", + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["lib", "node_modules"] +}