chore: knip fixes

chore: knip fixes
This commit is contained in:
Charlie Lamb
2026-01-28 11:22:16 +00:00
parent 6fe749496e
commit 4186090190
7 changed files with 365 additions and 6 deletions

View File

@@ -56,6 +56,7 @@
"@opentelemetry/semantic-conventions": "^1.34.0",
"@posthog/ai": "^7.4.2",
"@puzzmo/revenue-cat-webhook-types": "^1.1.0",
"@react-email/components": "^1.0.6",
"@sentry/bun": "catalog:",
"@supabase/supabase-js": "^2.46.2",
"@types/qs": "^6.14.0",
@@ -166,6 +167,7 @@
"@sentry/vite-plugin": "^4.6.0",
"@squircle/core": "^1.0.6",
"@squircle/paint-polyfill": "^1.0.6",
"@squircle/tailwindcss": "^1.0.6",
"@tailwindcss/vite": "^4.0.13",
"@tanstack/react-form": "^1.23.8",
"@tanstack/react-query": "^5.85.6",
@@ -203,6 +205,9 @@
"svix-react": "^1.13.3",
"swr": "^2.3.3",
"tailwind-merge": "^3.0.2",
"tailwind-scrollbar-hide": "^4.0.0",
"tailwindcss": "^4.0.13",
"tailwindcss-animate": "^1.0.7",
"use-stick-to-bottom": "^1.1.1",
"zod": "^3.25.23",
"zustand": "^5.0.8",
@@ -1026,7 +1031,47 @@
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
"@react-email/render": ["@react-email/render@1.1.2", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3", "react-promise-suspense": "^0.3.4" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw=="],
"@react-email/body": ["@react-email/body@0.2.1", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-ljDiQiJDu/Fq//vSIIP0z5Nuvt4+DX1RqGasstChDGJB/14ogd4VdNS9aacoede/ZjGy3o3Qb+cxyS+XgM6SwQ=="],
"@react-email/button": ["@react-email/button@0.2.1", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-qXyj7RZLE7POy9BMKSoqQ00tOXThjOZSUnI2Yu9i29IHngPlmrNayIWBoVKtElES7OWwypUcpiajwi1mUWx6/A=="],
"@react-email/code-block": ["@react-email/code-block@0.2.1", "", { "dependencies": { "prismjs": "^1.30.0" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-M3B7JpVH4ytgn83/ujRR1k1DQHvTeABiDM61OvAbjLRPhC/5KLHU5KkzIbbuGIrjWwxAbL1kSQzU8MhLEtSxyw=="],
"@react-email/code-inline": ["@react-email/code-inline@0.0.6", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-jfhebvv3dVsp3OdPgKXnk8+e2pBiDVZejDOBFzBa/IblrAJ9cQDkN6rBD5IyEg8hTOxwbw3iaI/yZFmDmIguIA=="],
"@react-email/column": ["@react-email/column@0.0.14", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-f+W+Bk2AjNO77zynE33rHuQhyqVICx4RYtGX9NKsGUg0wWjdGP0qAuIkhx9Rnmk4/hFMo1fUrtYNqca9fwJdHg=="],
"@react-email/components": ["@react-email/components@1.0.6", "", { "dependencies": { "@react-email/body": "0.2.1", "@react-email/button": "0.2.1", "@react-email/code-block": "0.2.1", "@react-email/code-inline": "0.0.6", "@react-email/column": "0.0.14", "@react-email/container": "0.0.16", "@react-email/font": "0.0.10", "@react-email/head": "0.0.13", "@react-email/heading": "0.0.16", "@react-email/hr": "0.0.12", "@react-email/html": "0.0.12", "@react-email/img": "0.0.12", "@react-email/link": "0.0.13", "@react-email/markdown": "0.0.18", "@react-email/preview": "0.0.14", "@react-email/render": "2.0.4", "@react-email/row": "0.0.13", "@react-email/section": "0.0.17", "@react-email/tailwind": "2.0.3", "@react-email/text": "0.1.6" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-3GwOeq+5yyiAcwSf7TnHi/HWKn22lXbwxQmkkAviSwZLlhsRVxvmWqRxvUVfQk/HclDUG+62+sGz9qjfb2Uxjw=="],
"@react-email/container": ["@react-email/container@0.0.16", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-QWBB56RkkU0AJ9h+qy33gfT5iuZknPC7Un/IjZv9B0QmMIK+WWacc0cH6y2SV5Cv/b99hU94fjEMOOO4enpkbQ=="],
"@react-email/font": ["@react-email/font@0.0.10", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-0urVSgCmQIfx5r7Xc586miBnQUVnGp3OTYUm8m5pwtQRdTRO5XrTtEfNJ3JhYhSOruV0nD8fd+dXtKXobum6tA=="],
"@react-email/head": ["@react-email/head@0.0.13", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-AJg6le/08Gz4tm+6MtKXqtNNyKHzmooOCdmtqmWxD7FxoAdU1eVcizhtQ0gcnVaY6ethEyE/hnEzQxt1zu5Kog=="],
"@react-email/heading": ["@react-email/heading@0.0.16", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-jmsKnQm1ykpBzw4hCYHwBkt5pW2jScXffPeEH5ZRF5tZeF5b1pvlFTO9han7C0pCkZYo1kEvWiRtx69yfCIwuw=="],
"@react-email/hr": ["@react-email/hr@0.0.12", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-TwmOmBDibavUQpXBxpmZYi2Iks/yeZOzFYh+di9EltMSnEabH8dMZXrl+pxNXzCgZ2XE8HY7VmUL65Lenfu5PA=="],
"@react-email/html": ["@react-email/html@0.0.12", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-KTShZesan+UsreU7PDUV90afrZwU5TLwYlALuCSU0OT+/U8lULNNbAUekg+tGwCnOfIKYtpDPKkAMRdYlqUznw=="],
"@react-email/img": ["@react-email/img@0.0.12", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-sRCpEARNVTf3FQhZOC+JTvu5r6ubiYWkT0ucYXg8ctkyi4G8QG+jgYPiNUqVeTLA2STOfmPM/nrk1nb84y6CPQ=="],
"@react-email/link": ["@react-email/link@0.0.13", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-lkWc/NjOcefRZMkQoSDDbuKBEBDES9aXnFEOuPH845wD3TxPwh+QTf0fStuzjoRLUZWpHnio4z7qGGRYusn/sw=="],
"@react-email/markdown": ["@react-email/markdown@0.0.18", "", { "dependencies": { "marked": "^15.0.12" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-gSuYK5fsMbGk87jDebqQ6fa2fKcWlkf2Dkva8kMONqLgGCq8/0d+ZQYMEJsdidIeBo3kmsnHZPrwdFB4HgjUXg=="],
"@react-email/preview": ["@react-email/preview@0.0.14", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-aYK8q0IPkBXyMsbpMXgxazwHxYJxTrXrV95GFuu2HbEiIToMwSyUgb8HDFYwPqqfV03/jbwqlsXmFxsOd+VNaw=="],
"@react-email/render": ["@react-email/render@2.0.4", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-kht2oTFQ1SwrLpd882ahTvUtNa9s53CERHstiTbzhm6aR2Hbykp/mQ4tpPvsBGkKAEvKRlDEoooh60Uk6nHK1g=="],
"@react-email/row": ["@react-email/row@0.0.13", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-bYnOac40vIKCId7IkwuLAAsa3fKfSfqCvv6epJKmPE0JBuu5qI4FHFCl9o9dVpIIS08s/ub+Y/txoMt0dYziGw=="],
"@react-email/section": ["@react-email/section@0.0.17", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-qNl65ye3W0Rd5udhdORzTV9ezjb+GFqQQSae03NDzXtmJq6sqVXNWNiVolAjvJNypim+zGXmv6J9TcV5aNtE/w=="],
"@react-email/tailwind": ["@react-email/tailwind@2.0.3", "", { "dependencies": { "tailwindcss": "^4.1.18" }, "peerDependencies": { "@react-email/body": "0.2.1", "@react-email/button": "0.2.1", "@react-email/code-block": "0.2.1", "@react-email/code-inline": "0.0.6", "@react-email/container": "0.0.16", "@react-email/heading": "0.0.16", "@react-email/hr": "0.0.12", "@react-email/img": "0.0.12", "@react-email/link": "0.0.13", "@react-email/preview": "0.0.14", "@react-email/text": "0.1.6", "react": "^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@react-email/body", "@react-email/button", "@react-email/code-block", "@react-email/code-inline", "@react-email/container", "@react-email/heading", "@react-email/hr", "@react-email/img", "@react-email/link", "@react-email/preview"] }, "sha512-URXb/T2WS4RlNGM5QwekYnivuiVUcU87H0y5sqLl6/Oi3bMmgL0Bmw/W9GeJylC+876Vw+E6NkE0uRiUFIQwGg=="],
"@react-email/text": ["@react-email/text@0.1.6", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-TYqkioRS45wTR5il3dYk/SbUjjEdhSwh9BtRNB99qNH1pXAwA45H7rAuxehiu8iJQJH0IyIr+6n62gBz9ezmsw=="],
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="],
@@ -1234,6 +1279,8 @@
"@squircle/paint-polyfill": ["@squircle/paint-polyfill@1.0.6", "", {}, "sha512-w3FotA1v53MG+R6eGM37wv1aVoW3jwymwGteGCGXEM10Gq7zYeCus5dTgcyW6fhc9piRqsFLyU8MIl+EbtM9RA=="],
"@squircle/tailwindcss": ["@squircle/tailwindcss@1.0.6", "", {}, "sha512-tmOWdpX7lzBAJzuvajn4NVBtC7mVORmzf/jKREYMw8iJ806Xq4FCUkrtbxkd9blzOx7TG2JwLF3avh5It/Z6TQ=="],
"@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
@@ -2662,6 +2709,8 @@
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
@@ -2950,8 +2999,12 @@
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
"tailwind-scrollbar-hide": ["tailwind-scrollbar-hide@4.0.0", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || >= 4.0.0 || >= 4.0.0-beta.8 || >= 4.0.0-alpha.20" } }, "sha512-gobtvVcThB2Dxhy0EeYSS1RKQJ5baDFkamkhwBvzvevwX6L4XQfpZ3me9s25Ss1ecFVT5jPYJ50n+7xTBJG9WQ=="],
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
"tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"tar-fs": ["tar-fs@3.1.1", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg=="],
@@ -3654,6 +3707,8 @@
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@react-email/markdown/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
"@reduxjs/toolkit/immer": ["immer@11.1.3", "", {}, "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q=="],
"@sentry-internal/browser-utils/@sentry/core": ["@sentry/core@10.37.0", "", {}, "sha512-hkRz7S4gkKLgPf+p3XgVjVm7tAfvcEPZxeACCC6jmoeKhGkzN44nXwLiqqshJ25RMcSrhfFvJa/FlBg6zupz7g=="],
@@ -3914,6 +3969,8 @@
"react-router/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"resend/@react-email/render": ["@react-email/render@1.1.2", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3", "react-promise-suspense": "^0.3.4" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw=="],
"router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],

View File

@@ -11,8 +11,10 @@
"src/_luaScriptsV2/**",
"src/utils/importUtils/**",
"src/utils/scriptUtils/**",
"src/utils/workerUtils/**"
]
"src/utils/workerUtils/**",
"src/db/**"
],
"ignoreDependencies": ["@react-email/components"]
},
"shared": {
"entry": ["index.ts"],
@@ -24,7 +26,12 @@
"entry": ["src/main.tsx"],
"project": ["src/**/*.{ts,tsx}"],
"ignore": ["src/components/ai-elements/**"],
"ignoreDependencies": ["tailwindcss"]
"ignoreDependencies": [
"tailwindcss",
"tailwind-scrollbar-hide",
"tailwindcss-animate",
"@squircle/tailwindcss"
]
},
"scripts": {
"entry": ["**/*.{ts,js}"],

View File

@@ -56,6 +56,7 @@
"@opentelemetry/semantic-conventions": "^1.34.0",
"@posthog/ai": "^7.4.2",
"@puzzmo/revenue-cat-webhook-types": "^1.1.0",
"@react-email/components": "^1.0.6",
"@sentry/bun": "catalog:",
"@supabase/supabase-js": "^2.46.2",
"@types/qs": "^6.14.0",

View File

@@ -0,0 +1,44 @@
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { sql } from "drizzle-orm";
import { db } from "./initDrizzle.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* Initialize all database functions (stored procedures)
* Loads SQL files in dependency order: helpers first, then main functions
*/
export const initializeDatabaseFunctions = async () => {
try {
console.log("Initializing database functions...");
const sqlPath = join(__dirname, "../internal/balances/utils/sql");
// Load SQL files in dependency order:
// 1. Helper functions (used by main functions)
// 2. Main functions (depend on helpers)
const sqlFiles = [
// Helper functions
"deductFromRollovers.sql",
"deductFromMainBalance.sql",
"getTotalBalance.sql",
"deductFromAdditionalBalance.sql",
"performDeduction.sql",
"syncBalances.sql",
"syncBalancesV2.sql",
];
for (const file of sqlFiles) {
const sqlContent = readFileSync(join(sqlPath, file), "utf-8");
await db.execute(sql.raw(sqlContent));
console.log(` ✓ Loaded ${file}`);
}
console.log("Database functions initialized successfully");
} catch (error) {
console.error("Failed to initialize database functions:", error);
throw error;
}
};

View File

@@ -0,0 +1,65 @@
import * as schema from "@autumn/shared";
import { is } from "drizzle-orm";
import { PgTable } from "drizzle-orm/pg-core";
import { logger } from "../external/logtail/logtailUtils";
import type { DrizzleCli } from "./initDrizzle";
const SKIP_TABLES = ["migrationErrors"];
export const validateDbSchema = async ({ db }: { db: DrizzleCli }) => {
// Dynamically get all tables from schema (exclude relations)
const tableEntries = Object.entries(schema)
.filter(([name, table]) => {
// Filter out relations and non-table exports
if (name.includes("Relations")) return false;
// Skip migrationErrors table (known issue)
if (SKIP_TABLES.includes(name)) return false;
return is(table, PgTable);
})
.map(([name, table]) => ({ name, table: table as PgTable }));
// Validate all tables by selecting all columns to ensure schema matches
// If schema mismatches, Drizzle will throw an error
const start = Date.now();
const results = await Promise.allSettled(
tableEntries.map(({ name, table }) =>
db
.select()
.from(table)
.limit(1)
.then(() => ({ name, success: true as const }))
.catch((err: Error) => ({
name,
success: false as const,
error: err.message,
})),
),
);
const elapsed = Date.now() - start;
// Check for any failures
const failures = results
.map((r) => (r.status === "fulfilled" ? r.value : null))
.filter(
(v): v is { name: string; success: false; error: string } =>
v !== null && !v.success,
);
if (failures.length > 0) {
const failureDetails = failures
.map((f) => `Table '${f.name}': ${f.error}`)
.join("; ");
logger.error(
`Health check failed - DB schema validation error: ${failureDetails}`,
);
throw new Error(
`Health check failed - DB schema validation error: ${failureDetails}`,
);
}
logger.info(
`Health check passed - DB schema validated for ${tableEntries.length} tables in ${elapsed}ms`,
);
return true;
};

View File

@@ -0,0 +1,182 @@
import { createHash } from "node:crypto";
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { sql } from "drizzle-orm";
import { logger } from "../external/logtail/logtailUtils";
import type { DrizzleCli } from "./initDrizzle";
type SqlFunction = {
name: string;
sourceFile: string;
content: string;
contentHash: string;
};
/**
* Dynamically discover SQL functions from the deductRpc folder
*/
const discoverSqlFunctions = (): SqlFunction[] => {
const __filename = fileURLToPath(import.meta.url);
const deductRpcPath = join(
__filename,
"../../internal/balances/track/trackUtils/deductRpc",
);
const sqlFiles = readdirSync(deductRpcPath).filter((file) =>
file.endsWith(".sql"),
);
return sqlFiles.map((file) => {
const filePath = join(deductRpcPath, file);
const content = readFileSync(filePath, "utf-8");
// Extract function name from CREATE FUNCTION statement
const functionNameMatch = content.match(
/CREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+(\w+)/i,
);
const functionName = functionNameMatch?.[1];
if (!functionName) {
throw new Error(`Could not extract function name from ${file}`);
}
// Create hash of normalized content (ignore whitespace differences)
const normalizedContent = content
.replace(/--.*$/gm, "") // Remove comments
.replace(/\s+/g, " ") // Normalize whitespace
.trim();
const contentHash = createHash("sha256")
.update(normalizedContent)
.digest("hex")
.substring(0, 16);
return {
name: functionName,
sourceFile: file,
content,
contentHash,
};
});
};
export const validateSqlFunctions = async ({
db,
validateContent = false,
}: {
db: DrizzleCli;
validateContent?: boolean;
}) => {
const start = Date.now();
// Dynamically discover SQL functions from source files
const requiredFunctions = discoverSqlFunctions();
logger.info(
`Discovered ${requiredFunctions.length} SQL functions from source files`,
);
// Query database for existing functions and their definitions
const result = await db.execute<{
function_name: string;
definition: string;
}>(sql`
SELECT
p.proname as function_name,
pg_get_functiondef(p.oid) as definition
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE n.nspname = 'public'
AND p.prokind = 'f'
ORDER BY p.proname;
`);
const dbFunctions = new Map(
result.map((row) => [row.function_name, row.definition]),
);
// Check for missing functions
const missingFunctions = requiredFunctions.filter(
(fn) => !dbFunctions.has(fn.name),
);
if (missingFunctions.length > 0) {
const missingDetails = missingFunctions
.map((fn) => `'${fn.name}' (from ${fn.sourceFile})`)
.join(", ");
logger.error(
`SQL function validation failed: Missing functions: ${missingDetails}`,
);
throw new Error(`Missing SQL functions: ${missingDetails}`);
}
// Optionally validate function content
const mismatchedFunctions: Array<{
name: string;
sourceFile: string;
reason: string;
}> = [];
if (validateContent) {
for (const fn of requiredFunctions) {
const dbDefinition = dbFunctions.get(fn.name);
if (!dbDefinition) continue;
// Normalize both definitions for comparison
const normalizeFunc = (str: string) =>
str
.replace(/--.*$/gm, "") // Remove comments
.replace(/\s+/g, " ") // Normalize whitespace
.toLowerCase()
.trim();
const normalizedSource = normalizeFunc(fn.content);
const normalizedDb = normalizeFunc(dbDefinition);
// Create hashes for comparison
const sourceHash = createHash("sha256")
.update(normalizedSource)
.digest("hex")
.substring(0, 16);
const dbHash = createHash("sha256")
.update(normalizedDb)
.digest("hex")
.substring(0, 16);
if (sourceHash !== dbHash) {
mismatchedFunctions.push({
name: fn.name,
sourceFile: fn.sourceFile,
reason: `Source hash: ${sourceHash}, DB hash: ${dbHash}`,
});
}
}
if (mismatchedFunctions.length > 0) {
const mismatchDetails = mismatchedFunctions
.map((fn) => `'${fn.name}' (${fn.sourceFile}): ${fn.reason}`)
.join("; ");
logger.warn(`SQL function content mismatch detected: ${mismatchDetails}`);
logger.warn(
"Functions exist but their content differs from source files. Run migrations to update.",
);
}
}
const elapsed = Date.now() - start;
logger.info(
`SQL function validation passed - ${requiredFunctions.length} functions verified in ${elapsed}ms${validateContent ? " (content validated)" : ""}`,
);
if (mismatchedFunctions.length > 0) {
logger.info(
`⚠️ ${mismatchedFunctions.length} function(s) have content differences`,
);
}
return true;
};

View File

@@ -17,7 +17,6 @@
"author": "Recase Inc.",
"license": "Apache-2.0",
"dependencies": {
"better-auth": "catalog:",
"@ai-sdk/react": "^3.0.25",
"@autumn/shared": "workspace:*",
"@better-auth/dash": "catalog:",
@@ -43,8 +42,8 @@
"@sentry/vite-plugin": "^4.6.0",
"@squircle/core": "^1.0.6",
"@squircle/paint-polyfill": "^1.0.6",
"@squircle/tailwindcss": "^1.0.6",
"@tailwindcss/vite": "^4.0.13",
"tailwindcss": "^4.0.13",
"@tanstack/react-form": "^1.23.8",
"@tanstack/react-query": "^5.85.6",
"@tanstack/react-table": "^8.21.3",
@@ -54,6 +53,7 @@
"ai": "^6.0.5",
"autumn-js": "^0.1.8",
"axios": "^1.8.3",
"better-auth": "catalog:",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -80,6 +80,9 @@
"svix-react": "^1.13.3",
"swr": "^2.3.3",
"tailwind-merge": "^3.0.2",
"tailwind-scrollbar-hide": "^4.0.0",
"tailwindcss": "^4.0.13",
"tailwindcss-animate": "^1.0.7",
"use-stick-to-bottom": "^1.1.1",
"zod": "^3.25.23",
"zustand": "^5.0.8"