diff --git a/next.config.mjs b/next.config.mjs index ac8490a3..792d21f1 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,8 +1,6 @@ -import dotenv from 'dotenv'; +import 'dotenv/config'; import { createRequire } from 'module'; -dotenv.config(); - const require = createRequire(import.meta.url); const pkg = require('./package.json'); diff --git a/package.json b/package.json index 7ff69489..f0000253 100644 --- a/package.json +++ b/package.json @@ -72,8 +72,8 @@ "@dicebear/core": "^9.2.1", "@fontsource/inter": "^4.5.15", "@hello-pangea/dnd": "^17.0.0", - "@prisma/client": "6.1.0", - "@prisma/extension-read-replicas": "^0.4.0", + "@prisma/client": "6.6.0", + "@prisma/extension-read-replicas": "^0.4.1", "@react-spring/web": "^9.7.3", "@tanstack/react-query": "^5.28.6", "@umami/prisma-client": "^0.14.0", @@ -107,7 +107,7 @@ "next": "15.3.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", - "prisma": "6.1.0", + "prisma": "6.6.0", "pure-rand": "^6.1.0", "react": "^19.0.0", "react-basics": "^0.126.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69cbbfcd..ada4d7c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,11 +27,11 @@ importers: specifier: ^17.0.0 version: 17.0.0(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@prisma/client': - specifier: 6.1.0 - version: 6.1.0(prisma@6.1.0) + specifier: 6.6.0 + version: 6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3) '@prisma/extension-read-replicas': - specifier: ^0.4.0 - version: 0.4.1(@prisma/client@6.1.0(prisma@6.1.0)) + specifier: ^0.4.1 + version: 0.4.1(@prisma/client@6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3)) '@react-spring/web': specifier: ^9.7.3 version: 9.7.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -40,7 +40,7 @@ importers: version: 5.74.3(react@19.1.0) '@umami/prisma-client': specifier: ^0.14.0 - version: 0.14.0(@prisma/client@6.1.0(prisma@6.1.0))(@prisma/extension-read-replicas@0.4.1(@prisma/client@6.1.0(prisma@6.1.0))) + version: 0.14.0(@prisma/client@6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3))(@prisma/extension-read-replicas@0.4.1(@prisma/client@6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3))) '@umami/redis-client': specifier: ^0.26.0 version: 0.26.0 @@ -132,8 +132,8 @@ importers: specifier: ^4.1.5 version: 4.1.5 prisma: - specifier: 6.1.0 - version: 6.1.0 + specifier: 6.6.0 + version: 6.6.0(typescript@5.8.3) pure-rand: specifier: ^6.1.0 version: 6.1.0 @@ -1878,34 +1878,40 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@prisma/client@6.1.0': - resolution: {integrity: sha512-AbQYc5+EJKm1Ydfq3KxwcGiy7wIbm4/QbjCKWWoNROtvy7d6a3gmAGkKjK0iUCzh+rHV8xDhD5Cge8ke/kiy5Q==} + '@prisma/client@6.6.0': + resolution: {integrity: sha512-vfp73YT/BHsWWOAuthKQ/1lBgESSqYqAWZEYyTdGXyFAHpmewwWL2Iz6ErIzkj4aHbuc6/cGSsE6ZY+pBO04Cg==} engines: {node: '>=18.18'} peerDependencies: prisma: '*' + typescript: '>=5.1.0' peerDependenciesMeta: prisma: optional: true + typescript: + optional: true - '@prisma/debug@6.1.0': - resolution: {integrity: sha512-0himsvcM4DGBTtvXkd2Tggv6sl2JyUYLzEGXXleFY+7Kp6rZeSS3hiTW9mwtUlXrwYbJP6pwlVNB7jYElrjWUg==} + '@prisma/config@6.6.0': + resolution: {integrity: sha512-d8FlXRHsx72RbN8nA2QCRORNv5AcUnPXgtPvwhXmYkQSMF/j9cKaJg+9VcUzBRXGy9QBckNzEQDEJZdEOZ+ubA==} - '@prisma/engines-version@6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959': - resolution: {integrity: sha512-PdJqmYM2Fd8K0weOOtQThWylwjsDlTig+8Pcg47/jszMuLL9iLIaygC3cjWJLda69siRW4STlCTMSgOjZzvKPQ==} + '@prisma/debug@6.6.0': + resolution: {integrity: sha512-DL6n4IKlW5k2LEXzpN60SQ1kP/F6fqaCgU/McgaYsxSf43GZ8lwtmXLke9efS+L1uGmrhtBUP4npV/QKF8s2ZQ==} - '@prisma/engines@6.1.0': - resolution: {integrity: sha512-GnYJbCiep3Vyr1P/415ReYrgJUjP79fBNc1wCo7NP6Eia0CzL2Ot9vK7Infczv3oK7JLrCcawOSAxFxNFsAERQ==} + '@prisma/engines-version@6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a': + resolution: {integrity: sha512-JzRaQ5Em1fuEcbR3nUsMNYaIYrOT1iMheenjCvzZblJcjv/3JIuxXN7RCNT5i6lRkLodW5ojCGhR7n5yvnNKrw==} + + '@prisma/engines@6.6.0': + resolution: {integrity: sha512-nC0IV4NHh7500cozD1fBoTwTD1ydJERndreIjpZr/S3mno3P6tm8qnXmIND5SwUkibNeSJMpgl4gAnlqJ/gVlg==} '@prisma/extension-read-replicas@0.4.1': resolution: {integrity: sha512-mCMDloqUKUwx2o5uedTs1FHX3Nxdt1GdRMoeyp1JggjiwOALmIYWhxfIN08M2BZ0w8SKwvJqicJZMjkQYkkijw==} peerDependencies: '@prisma/client': ^6.5.0 - '@prisma/fetch-engine@6.1.0': - resolution: {integrity: sha512-asdFi7TvPlEZ8CzSZ/+Du5wZ27q6OJbRSXh+S8ISZguu+S9KtS/gP7NeXceZyb1Jv1SM1S5YfiCv+STDsG6rrg==} + '@prisma/fetch-engine@6.6.0': + resolution: {integrity: sha512-Ohfo8gKp05LFLZaBlPUApM0M7k43a0jmo86YY35u1/4t+vuQH9mRGU7jGwVzGFY3v+9edeb/cowb1oG4buM1yw==} - '@prisma/get-platform@6.1.0': - resolution: {integrity: sha512-ia8bNjboBoHkmKGGaWtqtlgQOhCi7+f85aOkPJKgNwWvYrT6l78KgojLekE8zMhVk0R9lWcifV0Pf8l3/15V0Q==} + '@prisma/get-platform@6.6.0': + resolution: {integrity: sha512-3qCwmnT4Jh5WCGUrkWcc6VZaw0JY7eWN175/pcb5Z6FiLZZ3ygY93UX0WuV41bG51a6JN/oBH0uywJ90Y+V5eA==} '@react-spring/animated@9.7.5': resolution: {integrity: sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==} @@ -3412,6 +3418,11 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + esbuild@0.25.2: resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==} engines: {node: '>=18'} @@ -5587,10 +5598,15 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - prisma@6.1.0: - resolution: {integrity: sha512-aFI3Yi+ApUxkwCJJwyQSwpyzUX7YX3ihzuHNHOyv4GJg3X5tQsmRaJEnZ+ZyfHpMtnyahhmXVfbTZ+lS8ZtfKw==} + prisma@6.6.0: + resolution: {integrity: sha512-SYCUykz+1cnl6Ugd8VUvtTQq5+j1Q7C0CtzKPjQ8JyA2ALh0EEJkMCS+KgdnvKW1lrxjtjCyJSHOOT236mENYg==} engines: {node: '>=18.18'} hasBin: true + peerDependencies: + typescript: '>=5.1.0' + peerDependenciesMeta: + typescript: + optional: true process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} @@ -8404,34 +8420,42 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@prisma/client@6.1.0(prisma@6.1.0)': + '@prisma/client@6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3)': optionalDependencies: - prisma: 6.1.0 + prisma: 6.6.0(typescript@5.8.3) + typescript: 5.8.3 - '@prisma/debug@6.1.0': {} - - '@prisma/engines-version@6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959': {} - - '@prisma/engines@6.1.0': + '@prisma/config@6.6.0': dependencies: - '@prisma/debug': 6.1.0 - '@prisma/engines-version': 6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959 - '@prisma/fetch-engine': 6.1.0 - '@prisma/get-platform': 6.1.0 + esbuild: 0.25.2 + esbuild-register: 3.6.0(esbuild@0.25.2) + transitivePeerDependencies: + - supports-color - '@prisma/extension-read-replicas@0.4.1(@prisma/client@6.1.0(prisma@6.1.0))': - dependencies: - '@prisma/client': 6.1.0(prisma@6.1.0) + '@prisma/debug@6.6.0': {} - '@prisma/fetch-engine@6.1.0': - dependencies: - '@prisma/debug': 6.1.0 - '@prisma/engines-version': 6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959 - '@prisma/get-platform': 6.1.0 + '@prisma/engines-version@6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a': {} - '@prisma/get-platform@6.1.0': + '@prisma/engines@6.6.0': dependencies: - '@prisma/debug': 6.1.0 + '@prisma/debug': 6.6.0 + '@prisma/engines-version': 6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a + '@prisma/fetch-engine': 6.6.0 + '@prisma/get-platform': 6.6.0 + + '@prisma/extension-read-replicas@0.4.1(@prisma/client@6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3))': + dependencies: + '@prisma/client': 6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3) + + '@prisma/fetch-engine@6.6.0': + dependencies: + '@prisma/debug': 6.6.0 + '@prisma/engines-version': 6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a + '@prisma/get-platform': 6.6.0 + + '@prisma/get-platform@6.6.0': + dependencies: + '@prisma/debug': 6.6.0 '@react-spring/animated@9.7.5(react@19.1.0)': dependencies: @@ -8954,10 +8978,10 @@ snapshots: '@typescript-eslint/types': 6.21.0 eslint-visitor-keys: 3.4.3 - '@umami/prisma-client@0.14.0(@prisma/client@6.1.0(prisma@6.1.0))(@prisma/extension-read-replicas@0.4.1(@prisma/client@6.1.0(prisma@6.1.0)))': + '@umami/prisma-client@0.14.0(@prisma/client@6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3))(@prisma/extension-read-replicas@0.4.1(@prisma/client@6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3)))': dependencies: - '@prisma/client': 6.1.0(prisma@6.1.0) - '@prisma/extension-read-replicas': 0.4.1(@prisma/client@6.1.0(prisma@6.1.0)) + '@prisma/client': 6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3) + '@prisma/extension-read-replicas': 0.4.1(@prisma/client@6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3)) chalk: 4.1.2 debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: @@ -10180,6 +10204,13 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + esbuild-register@3.6.0(esbuild@0.25.2): + dependencies: + debug: 4.4.0(supports-color@8.1.1) + esbuild: 0.25.2 + transitivePeerDependencies: + - supports-color + esbuild@0.25.2: optionalDependencies: '@esbuild/aix-ppc64': 0.25.2 @@ -12658,11 +12689,15 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - prisma@6.1.0: + prisma@6.6.0(typescript@5.8.3): dependencies: - '@prisma/engines': 6.1.0 + '@prisma/config': 6.6.0 + '@prisma/engines': 6.6.0 optionalDependencies: fsevents: 2.3.3 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color process@0.11.10: {} diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 00000000..4756a226 --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,7 @@ +import path from 'node:path'; +import { defineConfig } from 'prisma/config'; + +export default defineConfig({ + earlyAccess: true, + schema: path.join('prisma', 'schema.prisma'), +}); diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 06800ecf..509c651e 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -1,5 +1,6 @@ import debug from 'debug'; -import prisma from '@umami/prisma-client'; +import { PrismaClient } from '@prisma/client'; +import { readReplicas } from '@prisma/extension-read-replicas'; import { formatInTimeZone } from 'date-fns-tz'; import { MYSQL, POSTGRESQL, getDatabaseType } from '@/lib/db'; import { SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants'; @@ -10,6 +11,16 @@ import { filtersToArray } from './params'; const log = debug('umami:prisma'); +const PRISMA = 'prisma'; +const PRISMA_LOG_OPTIONS = { + log: [ + { + emit: 'event', + level: 'query', + }, + ], +}; + const MYSQL_DATE_FORMATS = { minute: '%Y-%m-%dT%H:%i:00', hour: '%Y-%m-%d %H:00:00', @@ -234,14 +245,16 @@ async function rawQuery(sql: string, data: object): Promise { return db === MYSQL ? '?' : `$${params.length}${type ?? ''}`; }); - return prisma.rawQuery(query, params); + return process.env.DATABASE_REPLICA_URL + ? client.$replica().$queryRawUnsafe(query, params) + : client.$queryRawUnsafe(query, params); } async function pagedQuery(model: string, criteria: T, pageParams: PageParams) { const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams || {}; const size = +pageSize || DEFAULT_PAGE_SIZE; - const data = await prisma.client[model].findMany({ + const data = await client[model].findMany({ ...criteria, ...{ ...(size > 0 && { take: +size, skip: +size * (+page - 1) }), @@ -255,7 +268,7 @@ async function pagedQuery(model: string, criteria: T, pageParams: PageParams) }, }); - const count = await prisma.client[model].count({ where: (criteria as any).where }); + const count = await client[model].count({ where: (criteria as any).where }); return { data, count, page: +page, pageSize: size, orderBy }; } @@ -323,8 +336,51 @@ function getSearchParameters(query: string, filters: { [key: string]: any }[]) { }; } +function transaction(input: any, options?: any) { + return client.$transaction(input, options); +} + +function getClient(params?: { + logQuery?: boolean; + queryLogger?: () => void; + replicaUrl?: string; + options?: any; +}): PrismaClient { + const { + logQuery = !!process.env.LOG_QUERY, + queryLogger, + replicaUrl = process.env.DATABASE_REPLICA_URL, + options, + } = params || {}; + + const prisma = new PrismaClient({ + errorFormat: 'pretty', + ...(logQuery && PRISMA_LOG_OPTIONS), + ...options, + }); + + if (replicaUrl) { + prisma.$extends( + readReplicas({ + url: replicaUrl, + }), + ); + } + + if (logQuery) { + prisma.$on('query' as never, queryLogger || log); + } + + log('Prisma initialized'); + + return prisma; +} + +const client = global[PRISMA] || getClient(); + export default { - ...prisma, + client, + transaction, getAddIntervalQuery, getCastColumnQuery, getDayDiffQuery,