diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f6ff652 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +node_modules +.next +.git +*.md diff --git a/.gitignore b/.gitignore index 5ef6a52..f390d12 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +/src/generated/prisma diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..611acfc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +FROM node:20-alpine AS base + +FROM base AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npx prisma generate +RUN npm run build + +FROM base AS runner +WORKDIR /app +ENV NODE_ENV=production +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" +CMD ["node", "server.js"] diff --git a/next.config.ts b/next.config.ts index e9ffa30..5194c68 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,8 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + output: "standalone", + serverExternalPackages: ["@prisma/adapter-pg", "pg"], }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 1aa3d9c..2dd3f3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,19 @@ "name": "finance-app", "version": "0.1.0", "dependencies": { + "@prisma/adapter-pg": "^7.4.2", + "@prisma/client": "^7.4.2", + "@tanstack/react-query": "^5.90.21", "next": "16.1.6", + "pg": "^8.20.0", + "prisma": "^7.4.2", "react": "19.2.3", "react-dom": "19.2.3" }, "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", + "@types/pg": "^8.18.0", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", @@ -276,6 +282,66 @@ "node": ">=6.9.0" } }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", + "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", + "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/types": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", + "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", + "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite": { + "version": "0.3.15", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz", + "integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==", + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.0.20.tgz", + "integrity": "sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==", + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.3.15" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.20.tgz", + "integrity": "sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A==", + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.3.15" + } + }, "node_modules/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -453,6 +519,18 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1021,6 +1099,19 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mrleebo/prisma-ast": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.13.1.tgz", + "integrity": "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==", + "license": "MIT", + "dependencies": { + "chevrotain": "^10.5.0", + "lilconfig": "^2.1.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -1226,6 +1317,179 @@ "node": ">=12.4.0" } }, + "node_modules/@prisma/adapter-pg": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.4.2.tgz", + "integrity": "sha512-oUo2Zhe9Tf6YwVL8kLPuOLTK1Z2pwi/Ua77t2PuGyBan2w7shRKqHvYK+3XXmRH9RWhPJ4SMtHZKpNo6Ax/4bQ==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.4.2", + "pg": "^8.16.3", + "postgres-array": "3.0.4" + } + }, + "node_modules/@prisma/client": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.4.2.tgz", + "integrity": "sha512-ts2mu+cQHriAhSxngO3StcYubBGTWDtu/4juZhXCUKOwgh26l+s4KD3vT2kMUzFyrYnll9u/3qWrtzRv9CGWzA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.4.2" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.4.2.tgz", + "integrity": "sha512-cID+rzOEb38VyMsx5LwJMEY4NGIrWCNpKu/0ImbeooQ2Px7TI+kOt7cm0NelxUzF2V41UVVXAmYjANZQtCu1/Q==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.4.2.tgz", + "integrity": "sha512-CftBjWxav99lzY1Z4oDgomdb1gh9BJFAOmWF6P2v1xRfXqQb56DfBub+QKcERRdNoAzCb3HXy3Zii8Vb4AsXhg==", + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.18.4", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.4.2.tgz", + "integrity": "sha512-aP7qzu+g/JnbF6U69LMwHoUkELiserKmWsE2shYuEpNUJ4GrtxBCvZwCyCBHFSH2kLTF2l1goBlBh4wuvRq62w==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.20.0.tgz", + "integrity": "sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ==", + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.3.15", + "@electric-sql/pglite-socket": "0.0.20", + "@electric-sql/pglite-tools": "0.2.20", + "@hono/node-server": "1.19.9", + "@mrleebo/prisma-ast": "0.13.1", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "foreground-child": "3.3.1", + "get-port-please": "3.2.0", + "hono": "4.11.4", + "http-status-codes": "2.3.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.33.4", + "std-env": "3.10.0", + "valibot": "1.2.0", + "zeptomatch": "2.1.0" + } + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.4.2.tgz", + "integrity": "sha512-REdjFpT/ye9KdDs+CXAXPIbMQkVLhne9G5Pe97sNY4Ovx4r2DAbWM9hOFvvB1Oq8H8bOCdu0Ri3AoGALquQqVw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.4.2" + } + }, + "node_modules/@prisma/engines": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.4.2.tgz", + "integrity": "sha512-B+ZZhI4rXlzjVqRw/93AothEKOU5/x4oVyJFGo9RpHPnBwaPwk4Pi0Q4iGXipKxeXPs/dqljgNBjK0m8nocOJA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.4.2", + "@prisma/engines-version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", + "@prisma/fetch-engine": "7.4.2", + "@prisma/get-platform": "7.4.2" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919.tgz", + "integrity": "sha512-5FIKY3KoYQlBuZC2yc16EXfVRQ8HY+fLqgxkYfWCtKhRb3ajCRzP/rPeoSx11+NueJDANdh4hjY36mdmrTcGSg==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.4.2.tgz", + "integrity": "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.4.2" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.4.2.tgz", + "integrity": "sha512-f/c/MwYpdJO7taLETU8rahEstLeXfYgQGlz5fycG7Fbmva3iPdzGmjiSWHeSWIgNnlXnelUdCJqyZnFocurZuA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.4.2", + "@prisma/engines-version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", + "@prisma/get-platform": "7.4.2" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.4.2.tgz", + "integrity": "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.4.2" + } + }, + "node_modules/@prisma/get-platform": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", + "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.2.0" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", + "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/studio-core": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.13.1.tgz", + "integrity": "sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg==", + "license": "Apache-2.0", + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1233,6 +1497,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1513,6 +1783,32 @@ "tailwindcss": "4.2.1" } }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -1555,11 +1851,22 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/pg": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.18.0.tgz", + "integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2405,6 +2712,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/axe-core": { "version": "4.11.1", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", @@ -2502,6 +2818,34 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2599,6 +2943,44 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chevrotain": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", + "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "10.5.0", + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "@chevrotain/utils": "10.5.0", + "lodash": "4.17.21", + "regexp-to-ast": "0.5.0" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -2632,6 +3014,21 @@ "dev": true, "license": "MIT" }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2643,7 +3040,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2658,7 +3054,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -2747,6 +3142,15 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -2783,6 +3187,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2806,6 +3231,18 @@ "node": ">=0.10.0" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2821,6 +3258,16 @@ "node": ">= 0.4" } }, + "node_modules/effect": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", + "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.307", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", @@ -2835,6 +3282,15 @@ "dev": true, "license": "MIT" }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/enhanced-resolve": { "version": "5.20.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", @@ -3479,6 +3935,34 @@ "node": ">=0.10.0" } }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3620,6 +4104,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3661,6 +4161,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -3706,6 +4215,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "license": "MIT" + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -3751,6 +4266,23 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3811,9 +4343,20 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, + "node_modules/grammex": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", + "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", + "license": "MIT" + }, + "node_modules/graphmatch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "license": "MIT" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -3925,6 +4468,37 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hono": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", + "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4247,6 +4821,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4403,7 +4983,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -4428,7 +5007,6 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -4822,6 +5400,15 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4838,6 +5425,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4845,6 +5438,12 @@ "dev": true, "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4868,6 +5467,21 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -4942,6 +5556,38 @@ "dev": true, "license": "MIT" }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -5083,6 +5729,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.36", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", @@ -5090,6 +5742,29 @@ "dev": true, "license": "MIT" }, + "node_modules/nypm": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "license": "MIT", + "dependencies": { + "citty": "^0.2.0", + "pathe": "^2.0.3", + "tinyexec": "^1.0.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", + "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5213,6 +5888,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5308,7 +5989,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5321,6 +6001,116 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-types/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5340,6 +6130,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5379,6 +6180,58 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5389,6 +6242,39 @@ "node": ">= 0.8.0" } }, + "node_modules/prisma": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.4.2.tgz", + "integrity": "sha512-2bP8Ruww3Q95Z2eH4Yqh4KAENRsj/SxbdknIVBfd6DmjPwmpsC4OVFMLOeHt6tM3Amh8ebjvstrUz3V/hOe1dA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "7.4.2", + "@prisma/dev": "0.20.0", + "@prisma/engines": "7.4.2", + "@prisma/studio-core": "0.13.1", + "mysql2": "3.15.3", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5401,6 +6287,23 @@ "react-is": "^16.13.1" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5411,6 +6314,22 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5432,6 +6351,16 @@ ], "license": "MIT" }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/react": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", @@ -5460,6 +6389,19 @@ "dev": true, "license": "MIT" }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5483,6 +6425,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -5504,6 +6452,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remeda": { + "version": "2.33.4", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", + "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remeda" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -5545,6 +6502,15 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -5635,6 +6601,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -5651,6 +6623,11 @@ "semver": "bin/semver.js" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -5762,7 +6739,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -5775,7 +6751,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5857,6 +6832,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5866,6 +6853,24 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -5873,6 +6878,12 @@ "dev": true, "license": "MIT" }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -6093,6 +7104,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -6294,7 +7314,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -6430,11 +7450,24 @@ "punycode": "^2.1.0" } }, + "node_modules/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -6545,6 +7578,15 @@ "node": ">=0.10.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -6565,6 +7607,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zeptomatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", + "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", + "license": "MIT", + "dependencies": { + "grammex": "^3.1.11", + "graphmatch": "^1.1.0" + } + }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", diff --git a/package.json b/package.json index cb58955..fd44287 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,19 @@ "lint": "eslint" }, "dependencies": { + "@prisma/adapter-pg": "^7.4.2", + "@prisma/client": "^7.4.2", + "@tanstack/react-query": "^5.90.21", "next": "16.1.6", + "pg": "^8.20.0", + "prisma": "^7.4.2", "react": "19.2.3", "react-dom": "19.2.3" }, "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", + "@types/pg": "^8.18.0", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..831a20f --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,14 @@ +// This file was generated by Prisma, and assumes you have installed the following: +// npm install --save-dev prisma dotenv +import "dotenv/config"; +import { defineConfig } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: process.env["DATABASE_URL"], + }, +}); diff --git a/prisma/migrations/0001_init/migration.sql b/prisma/migrations/0001_init/migration.sql new file mode 100644 index 0000000..c387dbf --- /dev/null +++ b/prisma/migrations/0001_init/migration.sql @@ -0,0 +1,14 @@ +-- Add merchant_normalized column to existing transactions table +ALTER TABLE transactions ADD COLUMN IF NOT EXISTS merchant_normalized TEXT; + +-- Create transaction_overrides table +CREATE TABLE IF NOT EXISTS transaction_overrides ( + id SERIAL PRIMARY KEY, + transaction_id INTEGER NOT NULL UNIQUE REFERENCES transactions(id) ON DELETE CASCADE, + merchant_normalized TEXT, + category_override TEXT, + notes TEXT, + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_overrides_txn_id ON transaction_overrides(transaction_id); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..99e4f20 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..0c99912 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,17 @@ +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" +} + +datasource db { + provider = "postgresql" +} + +model transaction_overrides { + id Int @id @default(autoincrement()) + transaction_id Int @unique + merchant_normalized String? + category_override String? + notes String? + updated_at DateTime @default(now()) @updatedAt +} diff --git a/src/app/api/merchants/route.ts b/src/app/api/merchants/route.ts new file mode 100644 index 0000000..3739d90 --- /dev/null +++ b/src/app/api/merchants/route.ts @@ -0,0 +1,16 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getMerchantSuggestions, getBankNames } from "@/lib/queries"; + +export async function GET(req: NextRequest) { + const search = req.nextUrl.searchParams.get("search"); + const type = req.nextUrl.searchParams.get("type"); + + if (type === "banks") { + const banks = await getBankNames(); + return NextResponse.json(banks.map((b) => b.bank_name)); + } + + if (!search) return NextResponse.json([]); + const merchants = await getMerchantSuggestions(search); + return NextResponse.json(merchants.map((m) => m.merchant)); +} diff --git a/src/app/api/statements/[id]/route.ts b/src/app/api/statements/[id]/route.ts new file mode 100644 index 0000000..7134646 --- /dev/null +++ b/src/app/api/statements/[id]/route.ts @@ -0,0 +1,12 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getStatementById } from "@/lib/queries"; + +export async function GET( + _req: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params; + const stmt = await getStatementById(Number(id)); + if (!stmt) return NextResponse.json({ error: "Not found" }, { status: 404 }); + return NextResponse.json(stmt); +} diff --git a/src/app/api/statements/route.ts b/src/app/api/statements/route.ts new file mode 100644 index 0000000..18548e2 --- /dev/null +++ b/src/app/api/statements/route.ts @@ -0,0 +1,7 @@ +import { NextResponse } from "next/server"; +import { getStatements } from "@/lib/queries"; + +export async function GET() { + const statements = await getStatements(); + return NextResponse.json(statements); +} diff --git a/src/app/api/transactions/[id]/route.ts b/src/app/api/transactions/[id]/route.ts new file mode 100644 index 0000000..7570dca --- /dev/null +++ b/src/app/api/transactions/[id]/route.ts @@ -0,0 +1,46 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getTransactionById } from "@/lib/queries"; +import { prisma } from "@/lib/db"; + +export async function GET( + _req: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params; + const txn = await getTransactionById(Number(id)); + if (!txn) return NextResponse.json({ error: "Not found" }, { status: 404 }); + return NextResponse.json(txn); +} + +export async function PATCH( + req: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params; + const transactionId = Number(id); + const body = await req.json(); + + const { category, merchant_normalized, notes } = body as { + category?: string; + merchant_normalized?: string; + notes?: string; + }; + + const data: Record = { updated_at: new Date() }; + if (category !== undefined) data.category_override = category; + if (merchant_normalized !== undefined) data.merchant_normalized = merchant_normalized; + if (notes !== undefined) data.notes = notes; + + const override = await prisma.transaction_overrides.upsert({ + where: { transaction_id: transactionId }, + update: data, + create: { + transaction_id: transactionId, + category_override: category || null, + merchant_normalized: merchant_normalized || null, + notes: notes || null, + }, + }); + + return NextResponse.json(override); +} diff --git a/src/app/api/transactions/bulk/route.ts b/src/app/api/transactions/bulk/route.ts new file mode 100644 index 0000000..2c5605b --- /dev/null +++ b/src/app/api/transactions/bulk/route.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; + +export async function POST(req: NextRequest) { + const body = await req.json(); + const { action, ids, category, merchant_normalized } = body as { + action: string; + ids: number[]; + category?: string; + merchant_normalized?: string; + }; + + if (!ids || !Array.isArray(ids) || ids.length === 0) { + return NextResponse.json({ error: "ids required" }, { status: 400 }); + } + + if (action === "categorize" && category) { + const ops = ids.map((id) => + prisma.transaction_overrides.upsert({ + where: { transaction_id: id }, + update: { category_override: category, updated_at: new Date() }, + create: { transaction_id: id, category_override: category }, + }) + ); + await prisma.$transaction(ops); + return NextResponse.json({ updated: ids.length }); + } + + if (action === "normalize" && merchant_normalized) { + const ops = ids.map((id) => + prisma.transaction_overrides.upsert({ + where: { transaction_id: id }, + update: { merchant_normalized, updated_at: new Date() }, + create: { transaction_id: id, merchant_normalized }, + }) + ); + await prisma.$transaction(ops); + return NextResponse.json({ updated: ids.length }); + } + + return NextResponse.json({ error: "Invalid action" }, { status: 400 }); +} diff --git a/src/app/api/transactions/route.ts b/src/app/api/transactions/route.ts new file mode 100644 index 0000000..6b69a80 --- /dev/null +++ b/src/app/api/transactions/route.ts @@ -0,0 +1,21 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getTransactions } from "@/lib/queries"; + +export async function GET(req: NextRequest) { + const sp = req.nextUrl.searchParams; + + const result = await getTransactions({ + from: sp.get("from") || undefined, + to: sp.get("to") || undefined, + category: sp.get("category") || undefined, + bank_name: sp.get("bank_name") || undefined, + search: sp.get("search") || undefined, + statement_id: sp.get("statement_id") || undefined, + sort_by: sp.get("sort_by") || undefined, + sort_dir: sp.get("sort_dir") || undefined, + limit: sp.get("limit") ? Number(sp.get("limit")) : undefined, + offset: sp.get("offset") ? Number(sp.get("offset")) : undefined, + }); + + return NextResponse.json(result); +} diff --git a/src/app/budget/page.tsx b/src/app/budget/page.tsx new file mode 100644 index 0000000..06bd120 --- /dev/null +++ b/src/app/budget/page.tsx @@ -0,0 +1,8 @@ +export default function BudgetPage() { + return ( +
+

Budget

+

Coming soon - monthly budgets and analytics.

+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..575dbec 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,6 @@ @import "tailwindcss"; -:root { - --background: #ffffff; - --foreground: #171717; -} - @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); } - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..021f703 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import { Providers } from "@/components/providers"; +import { Sidebar } from "@/components/sidebar"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -13,8 +15,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Finance", + description: "Personal Finance Dashboard", }; export default function RootLayout({ @@ -23,11 +25,16 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + - {children} + +
+ +
{children}
+
+
); diff --git a/src/app/page.tsx b/src/app/page.tsx index 295f8fd..079556d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,65 +1,5 @@ -import Image from "next/image"; +import { redirect } from "next/navigation"; export default function Home() { - return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
-
- - Vercel logomark - Deploy Now - - - Documentation - -
-
-
- ); + redirect("/transactions"); } diff --git a/src/app/rules/page.tsx b/src/app/rules/page.tsx new file mode 100644 index 0000000..cd6c5b6 --- /dev/null +++ b/src/app/rules/page.tsx @@ -0,0 +1,8 @@ +export default function RulesPage() { + return ( +
+

Rules

+

Coming soon - auto-classify transactions with rules.

+
+ ); +} diff --git a/src/app/shared/page.tsx b/src/app/shared/page.tsx new file mode 100644 index 0000000..607ec08 --- /dev/null +++ b/src/app/shared/page.tsx @@ -0,0 +1,8 @@ +export default function SharedPage() { + return ( +
+

Shared Expenses

+

Coming soon - track shared expenses and splits.

+
+ ); +} diff --git a/src/app/statements/page.tsx b/src/app/statements/page.tsx new file mode 100644 index 0000000..50a222f --- /dev/null +++ b/src/app/statements/page.tsx @@ -0,0 +1,77 @@ +"use client"; + +import Link from "next/link"; +import { useStatements } from "@/lib/hooks"; + +function formatDate(d: string | null) { + if (!d) return "-"; + return new Date(d).toLocaleDateString("en-AU", { + day: "2-digit", + month: "short", + year: "numeric", + }); +} + +function formatCurrency(amount: number | null, currency = "AUD") { + if (amount === null || amount === undefined) return "-"; + return new Intl.NumberFormat("en-AU", { + style: "currency", + currency, + }).format(amount); +} + +export default function StatementsPage() { + const { data: statements, isLoading } = useStatements(); + + return ( +
+

Statements

+ + {isLoading ? ( +

Loading...

+ ) : !statements?.length ? ( +

No statements found

+ ) : ( +
+ {statements.map((s) => ( +
+
+

{s.bank_name}

+ {s.currency} +
+ {s.card_name && ( +

{s.card_name}

+ )} +
+

Account: {s.account_number}

+

+ Period: {formatDate(s.billing_start_date)} - {formatDate(s.billing_end_date)} +

+

Due: {formatDate(s.payment_due_date)}

+
+
+
+

+ {formatCurrency(s.total_amount_due, s.currency)} +

+

+ {s.transaction_count} transactions +

+
+ + View + +
+
+ ))} +
+ )} +
+ ); +} diff --git a/src/app/tags/page.tsx b/src/app/tags/page.tsx new file mode 100644 index 0000000..7ab85a2 --- /dev/null +++ b/src/app/tags/page.tsx @@ -0,0 +1,8 @@ +export default function TagsPage() { + return ( +
+

Tags

+

Coming soon - tag transactions for trips, projects, and more.

+
+ ); +} diff --git a/src/app/transactions/page.tsx b/src/app/transactions/page.tsx new file mode 100644 index 0000000..a86554a --- /dev/null +++ b/src/app/transactions/page.tsx @@ -0,0 +1,358 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { useTransactions, useBanks, useUpdateTransaction, useBulkAction } from "@/lib/hooks"; +import { CATEGORIES, formatCategory } from "@/lib/categories"; + +function formatDate(d: string) { + return new Date(d).toLocaleDateString("en-AU", { + day: "2-digit", + month: "short", + year: "numeric", + }); +} + +function formatAmount(amount: number, type: string) { + const formatted = new Intl.NumberFormat("en-AU", { + style: "currency", + currency: "AUD", + }).format(amount); + return type === "debit" ? formatted : `+${formatted}`; +} + +function TypeBadge({ type }: { type: string }) { + const colors: Record = { + debit: "bg-red-900/30 text-red-400", + credit: "bg-green-900/30 text-green-400", + payment: "bg-blue-900/30 text-blue-400", + refund: "bg-emerald-900/30 text-emerald-400", + fee: "bg-yellow-900/30 text-yellow-400", + interest: "bg-orange-900/30 text-orange-400", + }; + return ( + + {type} + + ); +} + +function InlineEdit({ + value, + onSave, + type = "text", + options, +}: { + value: string; + onSave: (val: string) => void; + type?: "text" | "select"; + options?: { value: string; label: string }[]; +}) { + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(value); + + if (!editing) { + return ( + + ); + } + + const commit = () => { + if (draft !== value) onSave(draft); + setEditing(false); + }; + + if (type === "select" && options) { + return ( + + ); + } + + return ( + setDraft(e.target.value)} + onBlur={commit} + onKeyDown={(e) => { if (e.key === "Enter") commit(); if (e.key === "Escape") setEditing(false); }} + className="bg-zinc-800 border border-zinc-600 rounded px-1 py-0.5 text-sm w-full" + /> + ); +} + +export default function TransactionsPage() { + const [filters, setFilters] = useState({ + from: "", + to: "", + category: "", + bank_name: "", + search: "", + sort_by: "transaction_date", + sort_dir: "desc", + limit: 50, + offset: 0, + }); + const [selected, setSelected] = useState>(new Set()); + const [bulkCategory, setBulkCategory] = useState(""); + + const { data, isLoading } = useTransactions(filters); + const { data: banks } = useBanks(); + const updateTxn = useUpdateTransaction(); + const bulkAction = useBulkAction(); + + const toggleSelect = useCallback((id: number) => { + setSelected((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }, []); + + const toggleAll = useCallback(() => { + if (!data?.data) return; + setSelected((prev) => { + if (prev.size === data.data.length) return new Set(); + return new Set(data.data.map((t) => t.id)); + }); + }, [data]); + + const setPage = (newOffset: number) => { + setFilters((f) => ({ ...f, offset: newOffset })); + setSelected(new Set()); + }; + + const toggleSort = (col: string) => { + setFilters((f) => ({ + ...f, + sort_by: col, + sort_dir: f.sort_by === col && f.sort_dir === "desc" ? "asc" : "desc", + offset: 0, + })); + }; + + const categoryOptions = CATEGORIES.map((c) => ({ value: c, label: formatCategory(c) })); + + const totalPages = data ? Math.ceil(data.total / filters.limit) : 0; + const currentPage = Math.floor(filters.offset / filters.limit) + 1; + + return ( +
+

Transactions

+ + {/* Filter bar */} +
+ setFilters((f) => ({ ...f, search: e.target.value, offset: 0 }))} + className="bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm w-48" + /> + setFilters((f) => ({ ...f, from: e.target.value, offset: 0 }))} + className="bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm" + /> + setFilters((f) => ({ ...f, to: e.target.value, offset: 0 }))} + className="bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm" + /> + + +
+ + {/* Bulk action bar */} + {selected.size > 0 && ( +
+ {selected.size} selected + + + +
+ )} + + {/* Table */} +
+ + + + + + + + + + + + + + + {isLoading ? ( + + ) : !data?.data.length ? ( + + ) : ( + data.data.map((t) => ( + + + + + + + + + + + )) + )} + +
+ + toggleSort("transaction_date")} + > + Date {filters.sort_by === "transaction_date" && (filters.sort_dir === "desc" ? "\u2193" : "\u2191")} + DescriptionMerchant toggleSort("amount")} + > + Amount {filters.sort_by === "amount" && (filters.sort_dir === "desc" ? "\u2193" : "\u2191")} + TypeCategoryBank
Loading...
No transactions found
+ toggleSelect(t.id)} + className="accent-blue-600" + /> + {formatDate(t.transaction_date)}{t.description} +
+ updateTxn.mutate({ id: t.id, merchant_normalized: val })} + /> + {t.merchant_override && ( + + )} +
+
+ {formatAmount(t.amount, t.transaction_type)} + +
+ updateTxn.mutate({ id: t.id, category: val })} + type="select" + options={categoryOptions} + /> + {t.category_override && ( + + )} +
+
{t.bank_name}
+
+ + {/* Pagination */} + {data && data.total > filters.limit && ( +
+ + Showing {filters.offset + 1}-{Math.min(filters.offset + filters.limit, data.total)} of {data.total} + +
+ + + Page {currentPage} of {totalPages} + + +
+
+ )} +
+ ); +} diff --git a/src/components/providers.tsx b/src/components/providers.tsx new file mode 100644 index 0000000..22ce540 --- /dev/null +++ b/src/components/providers.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useState } from "react"; + +export function Providers({ children }: { children: React.ReactNode }) { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { staleTime: 30_000, refetchOnWindowFocus: false }, + }, + }) + ); + + return ( + {children} + ); +} diff --git a/src/components/sidebar.tsx b/src/components/sidebar.tsx new file mode 100644 index 0000000..84b2c9f --- /dev/null +++ b/src/components/sidebar.tsx @@ -0,0 +1,78 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +const NAV_ITEMS = [ + { href: "/transactions", label: "Transactions", icon: "receipt" }, + { href: "/statements", label: "Statements", icon: "file-text" }, + { href: "/shared", label: "Shared", icon: "users" }, + { href: "/budget", label: "Budget", icon: "bar-chart" }, + { href: "/tags", label: "Tags", icon: "tag" }, + { href: "/rules", label: "Rules", icon: "settings" }, +]; + +const ICONS: Record = { + receipt: ( + + + + ), + "file-text": ( + + + + ), + users: ( + + + + ), + "bar-chart": ( + + + + ), + tag: ( + + + + ), + settings: ( + + + + + ), +}; + +export function Sidebar() { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/src/lib/categories.ts b/src/lib/categories.ts new file mode 100644 index 0000000..bc34f62 --- /dev/null +++ b/src/lib/categories.ts @@ -0,0 +1,33 @@ +export const CATEGORIES = [ + "groceries", + "dining", + "transport", + "fuel", + "shopping", + "utilities", + "entertainment", + "travel", + "health", + "insurance", + "subscriptions", + "cash_advance", + "government", + "education", + "rent", + "transfers", + "income", + "personal_care", + "pets", + "gifts", + "charity", + "other", +] as const; + +export type Category = (typeof CATEGORIES)[number]; + +export function formatCategory(cat: string): string { + return cat + .split("_") + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" "); +} diff --git a/src/lib/db.ts b/src/lib/db.ts new file mode 100644 index 0000000..ab5f86a --- /dev/null +++ b/src/lib/db.ts @@ -0,0 +1,17 @@ +import { PrismaClient } from "@/generated/prisma/client"; +import { PrismaPg } from "@prisma/adapter-pg"; + +const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }; + +function createPrisma() { + const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); + return new PrismaClient({ adapter }); +} + +export const prisma = globalForPrisma.prisma || createPrisma(); + +if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; + +export async function queryRaw(sql: string, params: unknown[] = []): Promise { + return prisma.$queryRawUnsafe(sql, ...params); +} diff --git a/src/lib/hooks.ts b/src/lib/hooks.ts new file mode 100644 index 0000000..7274aec --- /dev/null +++ b/src/lib/hooks.ts @@ -0,0 +1,130 @@ +"use client"; + +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import type { TransactionRow, StatementRow } from "./queries"; + +interface TransactionsResponse { + data: TransactionRow[]; + total: number; + limit: number; + offset: number; +} + +interface TransactionFilters { + from?: string; + to?: string; + category?: string; + bank_name?: string; + search?: string; + statement_id?: string; + sort_by?: string; + sort_dir?: string; + limit?: number; + offset?: number; +} + +function buildParams(filters: TransactionFilters): string { + const params = new URLSearchParams(); + Object.entries(filters).forEach(([key, val]) => { + if (val !== undefined && val !== "") params.set(key, String(val)); + }); + return params.toString(); +} + +export function useTransactions(filters: TransactionFilters) { + return useQuery({ + queryKey: ["transactions", filters], + queryFn: async () => { + const res = await fetch(`/api/transactions?${buildParams(filters)}`); + return res.json(); + }, + }); +} + +export function useTransaction(id: number) { + return useQuery({ + queryKey: ["transaction", id], + queryFn: async () => { + const res = await fetch(`/api/transactions/${id}`); + return res.json(); + }, + }); +} + +export function useStatements() { + return useQuery({ + queryKey: ["statements"], + queryFn: async () => { + const res = await fetch("/api/statements"); + return res.json(); + }, + }); +} + +export function useStatement(id: number) { + return useQuery({ + queryKey: ["statement", id], + queryFn: async () => { + const res = await fetch(`/api/statements/${id}`); + return res.json(); + }, + }); +} + +export function useBanks() { + return useQuery({ + queryKey: ["banks"], + queryFn: async () => { + const res = await fetch("/api/merchants?type=banks"); + return res.json(); + }, + }); +} + +export function useUpdateTransaction() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async ({ + id, + ...data + }: { + id: number; + category?: string; + merchant_normalized?: string; + notes?: string; + }) => { + const res = await fetch(`/api/transactions/${id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + return res.json(); + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["transactions"] }); + qc.invalidateQueries({ queryKey: ["transaction"] }); + }, + }); +} + +export function useBulkAction() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (body: { + action: string; + ids: number[]; + category?: string; + merchant_normalized?: string; + }) => { + const res = await fetch("/api/transactions/bulk", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + return res.json(); + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["transactions"] }); + }, + }); +} diff --git a/src/lib/queries.ts b/src/lib/queries.ts new file mode 100644 index 0000000..018c722 --- /dev/null +++ b/src/lib/queries.ts @@ -0,0 +1,187 @@ +import { queryRaw } from "./db"; + +export interface TransactionRow { + id: number; + statement_id: number; + transaction_date: string; + description: string; + amount: number; + transaction_type: string; + merchant_name: string | null; + merchant_normalized: string | null; + location: string | null; + foreign_currency_amount: number | null; + foreign_currency_code: string | null; + category: string; + row_index: number; + created_at: string; + // override fields + category_override: string | null; + merchant_override: string | null; + notes: string | null; + effective_category: string; + effective_merchant: string; + // statement context + bank_name: string; +} + +export interface StatementRow { + id: number; + bank_name: string; + card_name: string | null; + account_number: string; + account_type: string | null; + billing_start_date: string | null; + billing_end_date: string | null; + total_amount_due: number; + minimum_amount_due: number | null; + payment_due_date: string; + opening_balance: number | null; + closing_balance: number | null; + total_credits: number | null; + total_debits: number | null; + interest_charged: number | null; + fees_charged: number | null; + credit_limit: number | null; + currency: string; + tier_used: string | null; + created_at: string; + transaction_count: number; +} + +interface TransactionFilters { + from?: string; + to?: string; + category?: string; + bank_name?: string; + search?: string; + statement_id?: string; + sort_by?: string; + sort_dir?: string; + limit?: number; + offset?: number; +} + +export async function getTransactions(filters: TransactionFilters) { + const conditions: string[] = []; + const params: unknown[] = []; + let paramIdx = 1; + + if (filters.from) { + conditions.push(`t.transaction_date >= $${paramIdx++}`); + params.push(filters.from); + } + if (filters.to) { + conditions.push(`t.transaction_date <= $${paramIdx++}`); + params.push(filters.to); + } + if (filters.category) { + conditions.push(`COALESCE(o.category_override, t.category) = $${paramIdx++}`); + params.push(filters.category); + } + if (filters.bank_name) { + conditions.push(`s.bank_name = $${paramIdx++}`); + params.push(filters.bank_name); + } + if (filters.search) { + conditions.push(`(t.description ILIKE $${paramIdx} OR t.merchant_name ILIKE $${paramIdx})`); + params.push(`%${filters.search}%`); + paramIdx++; + } + if (filters.statement_id) { + conditions.push(`t.statement_id = $${paramIdx++}`); + params.push(Number(filters.statement_id)); + } + + const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; + + const sortCol = filters.sort_by === "amount" ? "t.amount" : "t.transaction_date"; + const sortDir = filters.sort_dir === "asc" ? "ASC" : "DESC"; + const limit = filters.limit || 50; + const offset = filters.offset || 0; + + // Count query + const countSql = ` + SELECT COUNT(*)::int as total + FROM transactions t + LEFT JOIN transaction_overrides o ON o.transaction_id = t.id + JOIN statements s ON s.id = t.statement_id + ${where} + `; + const countResult = await queryRaw<{ total: number }>(countSql, params); + const total = countResult[0]?.total || 0; + + // Data query + const dataSql = ` + SELECT t.*, + o.category_override, o.merchant_normalized as merchant_override, o.notes, + COALESCE(o.category_override, t.category) as effective_category, + COALESCE(o.merchant_normalized, t.merchant_normalized, t.merchant_name) as effective_merchant, + s.bank_name + FROM transactions t + LEFT JOIN transaction_overrides o ON o.transaction_id = t.id + JOIN statements s ON s.id = t.statement_id + ${where} + ORDER BY ${sortCol} ${sortDir}, t.row_index ASC + LIMIT $${paramIdx++} OFFSET $${paramIdx++} + `; + params.push(limit, offset); + + const data = await queryRaw(dataSql, params); + + return { data, total, limit, offset }; +} + +export async function getTransactionById(id: number) { + const sql = ` + SELECT t.*, + o.category_override, o.merchant_normalized as merchant_override, o.notes, + COALESCE(o.category_override, t.category) as effective_category, + COALESCE(o.merchant_normalized, t.merchant_normalized, t.merchant_name) as effective_merchant, + s.bank_name + FROM transactions t + LEFT JOIN transaction_overrides o ON o.transaction_id = t.id + JOIN statements s ON s.id = t.statement_id + WHERE t.id = $1 + `; + const rows = await queryRaw(sql, [id]); + return rows[0] || null; +} + +export async function getStatements() { + const sql = ` + SELECT s.*, + (SELECT COUNT(*)::int FROM transactions t WHERE t.statement_id = s.id) as transaction_count + FROM statements s + ORDER BY s.billing_end_date DESC NULLS LAST, s.created_at DESC + `; + return queryRaw(sql); +} + +export async function getStatementById(id: number) { + const sql = ` + SELECT s.*, + (SELECT COUNT(*)::int FROM transactions t WHERE t.statement_id = s.id) as transaction_count + FROM statements s + WHERE s.id = $1 + `; + const rows = await queryRaw(sql, [id]); + return rows[0] || null; +} + +export async function getMerchantSuggestions(search: string) { + const sql = ` + SELECT DISTINCT COALESCE(o.merchant_normalized, t.merchant_normalized, t.merchant_name) as merchant + FROM transactions t + LEFT JOIN transaction_overrides o ON o.transaction_id = t.id + WHERE COALESCE(o.merchant_normalized, t.merchant_normalized, t.merchant_name) ILIKE $1 + ORDER BY merchant + LIMIT 20 + `; + return queryRaw<{ merchant: string }>(sql, [`%${search}%`]); +} + +export async function getBankNames() { + const sql = `SELECT DISTINCT bank_name FROM statements ORDER BY bank_name`; + return queryRaw<{ bank_name: string }>(sql); +}