From 5f6fc7ee42453f1223af878918b2363675789430 Mon Sep 17 00:00:00 2001 From: Maxime-J Date: Wed, 1 Apr 2026 22:13:03 +0200 Subject: [PATCH 01/59] Fix incomplete retention report --- .../[websiteId]/(reports)/retention/Retention.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/(reports)/retention/Retention.tsx b/src/app/(main)/websites/[websiteId]/(reports)/retention/Retention.tsx index 4b9053d51..89b14080b 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/retention/Retention.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/retention/Retention.tsx @@ -1,5 +1,6 @@ import { Column, Grid, Icon, Row, Text } from '@umami/react-zen'; import type { ReactNode } from 'react'; +import { differenceInCalendarDays } from 'date-fns'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { Panel } from '@/components/common/Panel'; import { useLocale, useMessages, useResultQuery } from '@/components/hooks'; @@ -45,8 +46,6 @@ export function Retention({ websiteId, days = DAYS, startDate, endDate }: Retent return arr; }, []) || []; - const totalDays = rows.length; - return ( {data && ( @@ -84,6 +83,7 @@ export function Retention({ websiteId, days = DAYS, startDate, endDate }: Retent ))} {rows.map(({ date, visitors, records }: any, rowIndex: number) => { + const maxDay = differenceInCalendarDays(endDate, new Date(date)); return ( {days.map(day => { - if (totalDays - rowIndex < day) { - return null; - } - const percentage = records.filter(a => a.day === day)[0]?.percentage; + if (day > maxDay) return null; + const percentage = records.find(a => a.day === day)?.percentage; return ( {percentage ? `${Number(percentage).toFixed(2)}%` : ''} From 16f79a494ab2d13dcbad779680e6fcbf747ce65d Mon Sep 17 00:00:00 2001 From: IEBqp <69092277+IEBqp@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:45:00 +0300 Subject: [PATCH 02/59] =?UTF-8?q?fix(i18n):=20update=20country=20name=20fr?= =?UTF-8?q?om=20Turkey=20to=20T=C3=BCrkiye?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updates the English locale to reflect the official 2022 ISO 3166-1 name change. - Uses the \u00fc Unicode escape sequence to ensure safe rendering. --- public/intl/country/en-GB.json | 2 +- public/intl/country/en-US.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/intl/country/en-GB.json b/public/intl/country/en-GB.json index 95a4e329a..77a95d973 100644 --- a/public/intl/country/en-GB.json +++ b/public/intl/country/en-GB.json @@ -227,7 +227,7 @@ "TO": "Tonga", "TT": "Trinidad & Tobago", "TN": "Tunisia", - "TR": "Turkey", + "TR": "T\u00fcrkiye", "TM": "Turkmenistan", "TC": "Turks & Caicos Islands", "TV": "Tuvalu", diff --git a/public/intl/country/en-US.json b/public/intl/country/en-US.json index 95a4e329a..77a95d973 100644 --- a/public/intl/country/en-US.json +++ b/public/intl/country/en-US.json @@ -227,7 +227,7 @@ "TO": "Tonga", "TT": "Trinidad & Tobago", "TN": "Tunisia", - "TR": "Turkey", + "TR": "T\u00fcrkiye", "TM": "Turkmenistan", "TC": "Turks & Caicos Islands", "TV": "Tuvalu", From c1395aafb45dcefff329d0579d0f71e109b5b94a Mon Sep 17 00:00:00 2001 From: nurlennart Date: Fri, 17 Apr 2026 08:49:57 +0200 Subject: [PATCH 03/59] fix: restore realtime link on active users indicator --- src/components/metrics/ActiveUsers.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/metrics/ActiveUsers.tsx b/src/components/metrics/ActiveUsers.tsx index 2baf3357c..f1aa989c6 100644 --- a/src/components/metrics/ActiveUsers.tsx +++ b/src/components/metrics/ActiveUsers.tsx @@ -1,6 +1,7 @@ import { StatusLight, Text } from '@umami/react-zen'; import { useMemo } from 'react'; -import { useActyiveUsersQuery, useMessages } from '@/components/hooks'; +import Link from '@/components/common/Link'; +import { useActiveUsersQuery, useMessages } from '@/components/hooks'; export function ActiveUsers({ websiteId, @@ -12,7 +13,7 @@ export function ActiveUsers({ refetchInterval?: number; }) { const { t, labels } = useMessages(); - const { data } = useActyiveUsersQuery(websiteId, { refetchInterval }); + const { data } = useActiveUsersQuery(websiteId, { refetchInterval }); const count = useMemo(() => { if (websiteId) { @@ -27,10 +28,12 @@ export function ActiveUsers({ } return ( - - - {count} {t(labels.online)} - - + + + + {count} {t(labels.online)} + + + ); -} +} \ No newline at end of file From 8d1a62fb7bc45f4fdbb08ae619ceb7ef5f8461d5 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 17 Apr 2026 11:24:57 -0700 Subject: [PATCH 04/59] getValues performance fix for relational --- src/queries/sql/getValues.ts | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/queries/sql/getValues.ts b/src/queries/sql/getValues.ts index 8573335bf..908be4126 100644 --- a/src/queries/sql/getValues.ts +++ b/src/queries/sql/getValues.ts @@ -1,8 +1,13 @@ import clickhouse from '@/lib/clickhouse'; +import { FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { QueryFilters } from '@/lib/types'; +const SESSION_DB_COLUMNS = new Set( + SESSION_COLUMNS.map(col => FILTER_COLUMNS[col as keyof typeof FILTER_COLUMNS]).filter(Boolean), +); + const FUNCTION_NAME = 'getValues'; export async function getValues( @@ -45,13 +50,27 @@ async function relationalQuery(websiteId: string, column: string, filters: Query } } + if (SESSION_DB_COLUMNS.has(column)) { + return rawQuery( + ` + select ${column} as "value", count(*) as "count" + from session + where website_id = {{websiteId::uuid}} + and created_at between {{startDate}} and {{endDate}} + ${searchQuery} + group by 1 + order by 2 desc + limit 10 + `, + { websiteId, startDate, endDate, search: `%${search}%`, ...params }, + FUNCTION_NAME, + ); + } + return rawQuery( ` select ${column} as "value", count(*) as "count" from website_event - inner join session - on session.session_id = website_event.session_id - and session.website_id = website_event.website_id where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} ${searchQuery} @@ -60,13 +79,7 @@ async function relationalQuery(websiteId: string, column: string, filters: Query order by 2 desc limit 10 `, - { - websiteId, - startDate, - endDate, - search: `%${search}%`, - ...params, - }, + { websiteId, startDate, endDate, search: `%${search}%`, ...params }, FUNCTION_NAME, ); } From 17c5c14f9fc454d99c35649332e3250b77c1adc9 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 17 Apr 2026 15:32:22 -0700 Subject: [PATCH 05/59] Updated outdated packages. --- package.json | 30 +- pnpm-lock.yaml | 1271 +++++++++++++++++++++++++----------------------- 2 files changed, 679 insertions(+), 622 deletions(-) diff --git a/package.json b/package.json index 1d6fc31ee..d04c2172f 100644 --- a/package.json +++ b/package.json @@ -56,12 +56,12 @@ "@dicebear/collection": "^9.4.2", "@dicebear/core": "^9.4.2", "@hello-pangea/dnd": "^17.0.0", - "@prisma/adapter-pg": "^7.6.0", - "@prisma/client": "^7.6.0", + "@prisma/adapter-pg": "^7.7.0", + "@prisma/client": "^7.7.0", "@prisma/extension-read-replicas": "^0.5.0", "@react-spring/web": "^10.0.3", "@svgr/cli": "^8.1.0", - "@tanstack/react-query": "^5.96.0", + "@tanstack/react-query": "^5.99.0", "@umami/react-zen": "^0.245.0", "bcryptjs": "^3.0.2", "chalk": "^5.6.2", @@ -76,31 +76,31 @@ "debug": "^4.4.3", "del": "^6.0.0", "detect-browser": "^5.2.0", - "dotenv": "^17.3.1", - "esbuild": "^0.27.4", + "dotenv": "^17.4.2", + "esbuild": "^0.27.7", "immer": "^11.1.4", "ipaddr.js": "^2.3.0", "is-ci": "^3.0.1", "is-docker": "^3.0.0", "is-localhost-ip": "^2.0.0", - "isbot": "^5.1.37", + "isbot": "^5.1.39", "jsonwebtoken": "^9.0.3", "jszip": "^3.10.1", "kafkajs": "^2.1.0", - "lucide-react": "^1.7.0", - "maxmind": "^5.0.5", + "lucide-react": "^1.8.0", + "maxmind": "^5.0.6", "next": "16.2.4", "next-intl": "4.8.3", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "papaparse": "^5.5.3", "pg": "^8.20.0", - "prisma": "^7.6.0", + "prisma": "^7.7.0", "pure-rand": "^7.0.1", "react": "^19.2.5", "react-dom": "^19.2.5", "react-error-boundary": "^4.0.4", - "react-resizable-panels": "^4.8.0", + "react-resizable-panels": "^4.10.0", "react-simple-maps": "^2.3.0", "react-use-measure": "^2.0.4", "react-window": "^1.8.6", @@ -110,14 +110,14 @@ "rrweb-player": "1.0.0-alpha.4", "semver": "^7.7.4", "serialize-error": "^12.0.0", - "thenby": "^1.3.4", + "thenby": "^1.4.0", "ua-parser-js": "^2.0.9", "uuid": "^13.0.0", "zod": "^4.3.6", "zustand": "^5.0.12" }, "devDependencies": { - "@biomejs/biome": "^2.4.10", + "@biomejs/biome": "^2.4.12", "@netlify/plugin-nextjs": "^5.15.9", "@rollup/plugin-alias": "^5.0.0", "@rollup/plugin-commonjs": "^25.0.4", @@ -127,7 +127,7 @@ "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.3.0", "@types/jest": "^30.0.0", - "@types/node": "^24.12.0", + "@types/node": "^24.12.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.2", "babel-plugin-react-compiler": "19.1.0-rc.2", @@ -135,7 +135,7 @@ "cypress": "^13.6.6", "dotenv-cli": "^11.0.0", "jest": "^29.7.0", - "postcss": "^8.5.8", + "postcss": "^8.5.10", "postcss-flexbugs-fixes": "^5.0.2", "postcss-import": "^15.1.0", "postcss-preset-env": "7.8.3", @@ -148,7 +148,7 @@ "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", "tar": "^7.5.13", - "ts-jest": "^29.4.6", + "ts-jest": "^29.4.9", "ts-morph": "^27.0.2", "ts-node": "^10.9.1", "tsup": "^8.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 740288d7a..767b812d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,14 +24,14 @@ importers: specifier: ^17.0.0 version: 17.0.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@prisma/adapter-pg': - specifier: ^7.6.0 - version: 7.6.0 + specifier: ^7.7.0 + version: 7.7.0 '@prisma/client': - specifier: ^7.6.0 - version: 7.6.0(prisma@7.6.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3) + specifier: ^7.7.0 + version: 7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3) '@prisma/extension-read-replicas': specifier: ^0.5.0 - version: 0.5.0(@prisma/client@7.6.0(prisma@7.6.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)) + version: 0.5.0(@prisma/client@7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)) '@react-spring/web': specifier: ^10.0.3 version: 10.0.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -39,8 +39,8 @@ importers: specifier: ^8.1.0 version: 8.1.0(typescript@5.9.3) '@tanstack/react-query': - specifier: ^5.96.0 - version: 5.96.0(react@19.2.5) + specifier: ^5.99.0 + version: 5.99.0(react@19.2.5) '@umami/react-zen': specifier: ^0.245.0 version: 0.245.0(@types/react@19.2.14)(immer@11.1.4)(react-aria-components@1.17.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.1.18)(use-sync-external-store@1.6.0(react@19.2.5)) @@ -84,11 +84,11 @@ importers: specifier: ^5.2.0 version: 5.3.0 dotenv: - specifier: ^17.3.1 - version: 17.3.1 + specifier: ^17.4.2 + version: 17.4.2 esbuild: - specifier: ^0.27.4 - version: 0.27.4 + specifier: ^0.27.7 + version: 0.27.7 immer: specifier: ^11.1.4 version: 11.1.4 @@ -105,8 +105,8 @@ importers: specifier: ^2.0.0 version: 2.0.0 isbot: - specifier: ^5.1.37 - version: 5.1.37 + specifier: ^5.1.39 + version: 5.1.39 jsonwebtoken: specifier: ^9.0.3 version: 9.0.3 @@ -117,11 +117,11 @@ importers: specifier: ^2.1.0 version: 2.2.4 lucide-react: - specifier: ^1.7.0 - version: 1.7.0(react@19.2.5) + specifier: ^1.8.0 + version: 1.8.0(react@19.2.5) maxmind: - specifier: ^5.0.5 - version: 5.0.5 + specifier: ^5.0.6 + version: 5.0.6 next: specifier: 16.2.4 version: 16.2.4(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -141,8 +141,8 @@ importers: specifier: ^8.20.0 version: 8.20.0 prisma: - specifier: ^7.6.0 - version: 7.6.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3) + specifier: ^7.7.0 + version: 7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3) pure-rand: specifier: ^7.0.1 version: 7.0.1 @@ -156,8 +156,8 @@ importers: specifier: ^4.0.4 version: 4.1.2(react@19.2.5) react-resizable-panels: - specifier: ^4.8.0 - version: 4.8.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: ^4.10.0 + version: 4.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react-simple-maps: specifier: ^2.3.0 version: 2.3.0(prop-types@15.8.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -186,8 +186,8 @@ importers: specifier: ^12.0.0 version: 12.0.0 thenby: - specifier: ^1.3.4 - version: 1.3.4 + specifier: ^1.4.0 + version: 1.4.0 ua-parser-js: specifier: ^2.0.9 version: 2.0.9 @@ -202,8 +202,8 @@ importers: version: 5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)) devDependencies: '@biomejs/biome': - specifier: ^2.4.10 - version: 2.4.10 + specifier: ^2.4.12 + version: 2.4.12 '@netlify/plugin-nextjs': specifier: ^5.15.9 version: 5.15.9 @@ -232,8 +232,8 @@ importers: specifier: ^30.0.0 version: 30.0.0 '@types/node': - specifier: ^24.12.0 - version: 24.12.0 + specifier: ^24.12.2 + version: 24.12.2 '@types/react': specifier: ^19.2.14 version: 19.2.14 @@ -254,19 +254,19 @@ importers: version: 11.0.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3)) + version: 29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) postcss: - specifier: ^8.5.8 - version: 8.5.8 + specifier: ^8.5.10 + version: 8.5.10 postcss-flexbugs-fixes: specifier: ^5.0.2 - version: 5.0.2(postcss@8.5.8) + version: 5.0.2(postcss@8.5.10) postcss-import: specifier: ^15.1.0 - version: 15.1.0(postcss@8.5.8) + version: 15.1.0(postcss@8.5.10) postcss-preset-env: specifier: 7.8.3 - version: 7.8.3(postcss@8.5.8) + version: 7.8.3(postcss@8.5.10) prompts: specifier: 2.4.2 version: 2.4.2 @@ -290,22 +290,22 @@ importers: version: 2.2.4(rollup@4.60.1) rollup-plugin-postcss: specifier: ^4.0.2 - version: 4.0.2(postcss@8.5.8)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3)) + version: 4.0.2(postcss@8.5.10)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) tar: specifier: ^7.5.13 version: 7.5.13 ts-jest: - specifier: ^29.4.6 - version: 29.4.6(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.27.4)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3) + specifier: ^29.4.9 + version: 29.4.9(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.27.7)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)))(typescript@5.9.3) ts-morph: specifier: ^27.0.2 version: 27.0.2 ts-node: specifier: ^10.9.1 - version: 10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3) + version: 10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3) tsup: specifier: ^8.5.0 - version: 8.5.1(@swc/core@1.15.11(@swc/helpers@0.5.21))(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1) + version: 8.5.1(@swc/core@1.15.11(@swc/helpers@0.5.21))(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1) tsx: specifier: ^4.19.0 version: 4.21.0 @@ -313,7 +313,44 @@ importers: specifier: ^5.9.3 version: 5.9.3 - dist: {} + dist: + dependencies: + chart.js: + specifier: ^4.5.0 + version: 4.5.1 + chartjs-adapter-date-fns: + specifier: ^3.0.0 + version: 3.0.0(chart.js@4.5.1)(date-fns@2.30.0) + colord: + specifier: ^2.9.2 + version: 2.9.3 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.3 + lucide-react: + specifier: ^0.542.0 + version: 0.542.0(react@19.2.5) + pure-rand: + specifier: ^7.0.1 + version: 7.0.1 + react-simple-maps: + specifier: ^2.3.0 + version: 2.3.0(prop-types@15.8.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react-use-measure: + specifier: ^2.0.4 + version: 2.1.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react-window: + specifier: ^1.8.6 + version: 1.8.11(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + serialize-error: + specifier: ^12.0.0 + version: 12.0.0 + thenby: + specifier: ^1.3.4 + version: 1.3.4 + uuid: + specifier: ^11.1.0 + version: 11.1.0 packages: @@ -498,59 +535,59 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@biomejs/biome@2.4.10': - resolution: {integrity: sha512-xxA3AphFQ1geij4JTHXv4EeSTda1IFn22ye9LdyVPoJU19fNVl0uzfEuhsfQ4Yue/0FaLs2/ccVi4UDiE7R30w==} + '@biomejs/biome@2.4.12': + resolution: {integrity: sha512-Rro7adQl3NLq/zJCIL98eElXKI8eEiBtoeu5TbXF/U3qbjuSc7Jb5rjUbeHHcquDWeSf3HnGP7XI5qGrlRk/pA==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.4.10': - resolution: {integrity: sha512-vuzzI1cWqDVzOMIkYyHbKqp+AkQq4K7k+UCXWpkYcY/HDn1UxdsbsfgtVpa40shem8Kax4TLDLlx8kMAecgqiw==} + '@biomejs/cli-darwin-arm64@2.4.12': + resolution: {integrity: sha512-BnMU4Pc3ciEVteVpZ0BK33MLr7X57F5w1dwDLDn+/iy/yTrA4Q/N2yftidFtsA4vrDh0FMXDpacNV/Tl3fbmng==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.4.10': - resolution: {integrity: sha512-14fzASRo+BPotwp7nWULy2W5xeUyFnTaq1V13Etrrxkrih+ez/2QfgFm5Ehtf5vSjtgx/IJycMMpn5kPd5ZNaA==} + '@biomejs/cli-darwin-x64@2.4.12': + resolution: {integrity: sha512-x9uJ0bI1rJsWICp3VH8w/5PnAVD3A7SqzDpbrfoUQX1QyWrK5jSU4fRLo/wSgGeplCivbxBRKmt5Xq4/nWvq8A==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.4.10': - resolution: {integrity: sha512-WrJY6UuiSD/Dh+nwK2qOTu8kdMDlLV3dLMmychIghHPAysWFq1/DGC1pVZx8POE3ZkzKR3PUUnVrtZfMfaJjyQ==} + '@biomejs/cli-linux-arm64-musl@2.4.12': + resolution: {integrity: sha512-FhfpkAAlKL6kwvcVap0Hgp4AhZmtd3YImg0kK1jd7C/aSoh4SfsB2f++yG1rU0lr8Y5MCFJrcSkmssiL9Xnnig==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [musl] - '@biomejs/cli-linux-arm64@2.4.10': - resolution: {integrity: sha512-7MH1CMW5uuxQ/s7FLST63qF8B3Hgu2HRdZ7tA1X1+mk+St4JOuIrqdhIBnnyqeyWJNI+Bww7Es5QZ0wIc1Cmkw==} + '@biomejs/cli-linux-arm64@2.4.12': + resolution: {integrity: sha512-tOwuCuZZtKi1jVzbk/5nXmIsziOB6yqN8c9r9QM0EJYPU6DpQWf11uBOSCfFKKM4H3d9ZoarvlgMfbcuD051Pw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.4.10': - resolution: {integrity: sha512-kDTi3pI6PBN6CiczsWYOyP2zk0IJI08EWEQyDMQWW221rPaaEz6FvjLhnU07KMzLv8q3qSuoB93ua6inSQ55Tw==} + '@biomejs/cli-linux-x64-musl@2.4.12': + resolution: {integrity: sha512-dwTIgZrGutzhkQCuvHynCkyW6hJxUuyZqKKO0YNfaS2GUoRO+tOvxXZqZB6SkWAOdfZTzwaw8IEdUnIkHKHoew==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [musl] - '@biomejs/cli-linux-x64@2.4.10': - resolution: {integrity: sha512-tZLvEEi2u9Xu1zAqRjTcpIDGVtldigVvzug2fTuPG0ME/g8/mXpRPcNgLB22bGn6FvLJpHHnqLnwliOu8xjYrg==} + '@biomejs/cli-linux-x64@2.4.12': + resolution: {integrity: sha512-8pFeAnLU9QdW9jCIslB/v82bI0lhBmz2ZAKc8pVMFPO0t0wAHsoEkrUQUbMkIorTRIjbqyNZHA3lEXavsPWYSw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [glibc] - '@biomejs/cli-win32-arm64@2.4.10': - resolution: {integrity: sha512-umwQU6qPzH+ISTf/eHyJ/QoQnJs3V9Vpjz2OjZXe9MVBZ7prgGafMy7yYeRGnlmDAn87AKTF3Q6weLoMGpeqdQ==} + '@biomejs/cli-win32-arm64@2.4.12': + resolution: {integrity: sha512-B0DLnx0vA9ya/3v7XyCaP+/lCpnbWbMOfUFFve+xb5OxyYvdHaS55YsSddr228Y+JAFk58agCuZTsqNiw2a6ig==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.4.10': - resolution: {integrity: sha512-aW/JU5GuyH4uxMrNYpoC2kjaHlyJGLgIa3XkhPEZI0uKhZhJZU8BuEyJmvgzSPQNGozBwWjC972RaNdcJ9KyJg==} + '@biomejs/cli-win32-x64@2.4.12': + resolution: {integrity: sha512-yMckRzTyZ83hkk8iDFWswqSdU8tvZxspJKnYNh7JZr/zhZNOlzH13k4ecboU6MurKExCe2HUkH75pGI/O2JwGA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -892,158 +929,158 @@ packages: '@epic-web/invariant@1.0.0': resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} - '@esbuild/aix-ppc64@0.27.4': - resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.4': - resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.4': - resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.4': - resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.4': - resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.4': - resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.4': - resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.4': - resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.4': - resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.4': - resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.4': - resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.4': - resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.4': - resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.4': - resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.4': - resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.4': - resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.4': - resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.4': - resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.4': - resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.4': - resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.4': - resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.4': - resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.4': - resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.4': - resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.4': - resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.4': - resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -1548,14 +1585,14 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@prisma/adapter-pg@7.6.0': - resolution: {integrity: sha512-BjHNmJqqa42NqJSDPnXUfwUofWo8LJY7Ui2gqxN4DmAOb+H/gGKv+hln2Xq/1kSJXPW5AXMXuNiPDMpywvyIOw==} + '@prisma/adapter-pg@7.7.0': + resolution: {integrity: sha512-q33Ta8sKbgzEpAy0lx45tAq//yMv0qcb+8nj+TCA3P4wiAY+OBFEFk/NDkZncAfHaNJeGo5WJpJdpbL+ijYx8g==} - '@prisma/client-runtime-utils@7.6.0': - resolution: {integrity: sha512-fD7jlqubsZvVODKvsp9lOpXVecx2aWGxC2l35Ioz2t+teUJ5CfR0SAMsi7UkU1VvaZmmm+DS6BdujF622nY7tQ==} + '@prisma/client-runtime-utils@7.7.0': + resolution: {integrity: sha512-BLyd0UpFYOtyJFTHm7jS9vesHW7P83abibodQMiIofqjBKzDHQ1VAsQkdfvXyYDkPlONPfOTz7/rv3x/+CQqvQ==} - '@prisma/client@7.6.0': - resolution: {integrity: sha512-7Pe/1ayh3GgWPEg4mmT4ax77LJ1wC+XlnIFvQ94bLP2DsUnOpnruQQR3Jw7r+Frthk94QqDNxo3FjSg8h9PXeQ==} + '@prisma/client@7.7.0': + resolution: {integrity: sha512-5Ar4OsZpJ54s21sy5oDNNW9gQtd4NuxCaiM7+JDTOU07D6VvlpLjYzAVCMB1+JzokN+08dAVomlx+b7bhJd3ww==} engines: {node: ^20.19 || ^22.12 || >=24.0} peerDependencies: prisma: '*' @@ -1566,40 +1603,40 @@ packages: typescript: optional: true - '@prisma/config@7.6.0': - resolution: {integrity: sha512-MuAz1MK4PeG5/03YzfzX3CnFVHQ6qePGwUpQRzPzX5tT0ffJ3Tzi9zJZbBc+VzEGFCM8ghW/gTVDR85Syjt+Yw==} + '@prisma/config@7.7.0': + resolution: {integrity: sha512-hmPI3tKLO2aP0Y5vugbjcnA9qqlfJndiT6ds4tw28U5hNHLWg+mHJEWAhjsSPgxjtmxhJ/EDIeIlyh+3Us0OPg==} '@prisma/debug@7.2.0': resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==} - '@prisma/debug@7.6.0': - resolution: {integrity: sha512-LpHr3qos4lQZ6sxwjStf59YBht7m9/QF7NSQsMH6qGENWZu2w3UkQUGn1h5iRkDjnWRj3VHykOu9qFhps4ADvA==} + '@prisma/debug@7.7.0': + resolution: {integrity: sha512-12J62XdqCmpiwJHhHdQxZeY3ckVCWIFmcJP8hg5dPTceeiQ0wiojXGFYTluKqFQfu46fRLgb/rLALZMAx3+dTA==} '@prisma/dev@0.24.3': resolution: {integrity: sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==} - '@prisma/driver-adapter-utils@7.6.0': - resolution: {integrity: sha512-D8j3p0RnhLuufMaRLX6QqtGgPC5Ao3l5oFP6Q5AL0rTHi4vna+NzGEipwCsfvcSvaGFCbsH3lsTMbb4WvY+ovA==} + '@prisma/driver-adapter-utils@7.7.0': + resolution: {integrity: sha512-gZXREeu6mOk7zXfGFJgh86p7Vhj0sXNKp+4Cg1tWYo7V2dfncP2qxS2BiTmbIIha8xPqItkl0WSw38RuSq1HoQ==} '@prisma/engines-version@7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711': resolution: {integrity: sha512-r51DLcJ8bDRSrBEJF3J4cinoWyGA7rfP2mG6lD90VqIbGNOkbfcLcXalSVjq5Y6brQS3vcjrq4GbyUb1Cb7vkw==} - '@prisma/engines@7.6.0': - resolution: {integrity: sha512-Sn5edRzhHqgRV2M+A0eIbY442B4mReWWf3pKs/LKreYgW7oa/up8JtK/s4iv/EQA097cyboZ08mmkpbLp+tZ3w==} + '@prisma/engines@7.7.0': + resolution: {integrity: sha512-7fmcbT7HHXBq/b+3h/dO1JI3fd8l8q7erf7xP7pRprh58hmSSnG8mg9K3yjW3h9WaHWUwngVFpSxxxivaitQ2w==} '@prisma/extension-read-replicas@0.5.0': resolution: {integrity: sha512-t0kLjqFMte4wwGrZFhya4iFoyoOY4kjlyyE60WXZL66PqP6PcBpW/35eKb/3UcxgjJxcDlaJKfmGokFLOyafiQ==} peerDependencies: '@prisma/client': ^7.0.0 - '@prisma/fetch-engine@7.6.0': - resolution: {integrity: sha512-N575Ni95c3FkduWY/eKTHqNYgNbceZ1tQaSknVtJjpKmiiBXmniESn/GTxsDvICC4ZeiNrXxioGInzQrCdx16w==} + '@prisma/fetch-engine@7.7.0': + resolution: {integrity: sha512-TfyzveBQoK4xALzsTpVhB/0KG1N8zOK0ap+RnBMkzGUu3f98fnQ4QtXa2wlKPhsO2X8a3N5ugFQgcKNoHGmDfw==} '@prisma/get-platform@7.2.0': resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==} - '@prisma/get-platform@7.6.0': - resolution: {integrity: sha512-ohZDwXvtmnbzOcutR2D13lDWpZP1wQjmPyztmt0AwXLzQI7q95EE7NYCvS+M6N6SivT+BM0NOqLmTH3wms4L3A==} + '@prisma/get-platform@7.7.0': + resolution: {integrity: sha512-MEUNzvKxvYnJ7kgvd6oNRnMmmiGNS9TYLB2weMeIXplnHdL/UWEGnvavYGnN7KLJ2n0iI4dDAyzSkHI3c7AscQ==} '@prisma/query-plan-executor@7.2.0': resolution: {integrity: sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==} @@ -2227,11 +2264,11 @@ packages: '@swc/types@0.1.25': resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} - '@tanstack/query-core@5.96.0': - resolution: {integrity: sha512-sfO3uQeol1BU7cRP6NYY7nAiX3GiNY20lI/dtSbKLwcIkYw/X+w/tEsQAkc544AfIhBX/IvH/QYtPHrPhyAKGw==} + '@tanstack/query-core@5.99.0': + resolution: {integrity: sha512-3Jv3WQG0BCcH7G+7lf/bP8QyBfJOXeY+T08Rin3GZ1bshvwlbPt7NrDHMEzGdKIOmOzvIQmxjk28YEQX60k7pQ==} - '@tanstack/react-query@5.96.0': - resolution: {integrity: sha512-6qbjdm1K5kizVKv9TNqhIN3doq2anRhdF2XaFMFSn4m8L22S69RV+FilvlyVT4RoJyMxtPU5rs4RpdFa/PEC7A==} + '@tanstack/react-query@5.99.0': + resolution: {integrity: sha512-OY2bCqPemT1LlqJ8Y2CUau4KELnIhhG9Ol3ZndPbdnB095pRbPo1cHuXTndg8iIwtoHTgwZjyaDnQ0xD0mYwAw==} peerDependencies: react: ^18 || ^19 @@ -2303,8 +2340,8 @@ packages: resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==} deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed. - '@types/node@24.12.0': - resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} + '@types/node@24.12.2': + resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} @@ -2684,8 +2721,8 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} - citty@0.2.1: - resolution: {integrity: sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==} + citty@0.2.2: + resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} @@ -3051,8 +3088,8 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} del@6.1.1: resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} @@ -3137,8 +3174,8 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} - dotenv@17.3.1: - resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} dunder-proto@1.0.1: @@ -3219,8 +3256,8 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild@0.27.4: - resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} engines: {node: '>=18'} hasBin: true @@ -3506,8 +3543,8 @@ packages: graphmatch@1.1.1: resolution: {integrity: sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==} - handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} engines: {node: '>=0.4.7'} hasBin: true @@ -3827,8 +3864,8 @@ packages: isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - isbot@5.1.37: - resolution: {integrity: sha512-5bcicX81xf6NlTEV8rWdg7Pk01LFizDetuYGHx6d/f6y3lR2/oo8IfxjzJqn1UdDEyCcwT9e7NRloj8DwCYujQ==} + isbot@5.1.39: + resolution: {integrity: sha512-obH0yYahGXdzNxo+djmHhBYThUKDkz565cxkIlt2L9hXfv1NlaLKoDBHo6KxXsYrIXx2RK3x5vY36CfZcobxEw==} engines: {node: '>=18'} isexe@2.0.0: @@ -4203,13 +4240,18 @@ packages: resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + lucide-react@0.542.0: + resolution: {integrity: sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lucide-react@0.555.0: resolution: {integrity: sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - lucide-react@1.7.0: - resolution: {integrity: sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==} + lucide-react@1.8.0: + resolution: {integrity: sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4233,8 +4275,8 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - maxmind@5.0.5: - resolution: {integrity: sha512-1lcH2kMjbBpCFhuHaMU32vz8CuOsKttRcWMQyXvtlklopCzN7NNHSVR/h9RYa8JPuFTGmkn2vYARm+7cIGuqDw==} + maxmind@5.0.6: + resolution: {integrity: sha512-5bvd/u+kIaTqaGM+xkXjatzQw1dQfSmlLggr2W1EKMyMxSgx2woZyusLpNpZ4DdPmL+1bbJWeo4LXsi6bC0Iew==} engines: {node: '>=12', npm: '>=6'} mdn-data@2.0.14: @@ -5076,8 +5118,8 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + postcss@8.5.10: + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -5125,8 +5167,8 @@ packages: resolution: {integrity: sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - prisma@7.6.0: - resolution: {integrity: sha512-OKJIPT81K3+F+AayIkY/Y3mkF2NWoFh7lZApaaqPYy7EHILKdO0VsmGkP+hDKYTySHsFSyLWXm/JgcR1B8fY1Q==} + prisma@7.7.0: + resolution: {integrity: sha512-HlgwRBt1uEFB9LStHL4HLYDvoi4BNu1rYA0hPG0zCAEyK9SaZBqp7E5Rjpc3Qh8Lex/ye/svoHZ0OWoFNhWxuQ==} engines: {node: ^20.19 || ^22.12 || >=24.0} hasBin: true peerDependencies: @@ -5233,8 +5275,8 @@ packages: redux: optional: true - react-resizable-panels@4.8.0: - resolution: {integrity: sha512-2uEABkewb3ky/ZgIlAUxWa1W/LjsK494fdV1QsXxst7CDRHCzo7h22tWWu3NNaBjmiuriOCt3CvhipnaYcpoIw==} + react-resizable-panels@4.10.0: + resolution: {integrity: sha512-frjewRQt7TCv/vCH1pJfjZ7RxAhr5pKuqVQtVgzFq/vherxBFOWyC3xMbryx5Ti2wylViGUFc93Etg4rB3E0UA==} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 @@ -5764,6 +5806,9 @@ packages: thenby@1.3.4: resolution: {integrity: sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==} + thenby@1.4.0: + resolution: {integrity: sha512-D+BkEl5MpeTbQ1Rz0io/TLZmwmYUAgvUpS1r2eKUvPicYOnpqzjZE6bVa7rSEYv189zkSM+CVc+hG5X7nVe9LQ==} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -5780,15 +5825,15 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - tiny-lru@11.4.7: - resolution: {integrity: sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw==} - engines: {node: '>=12'} + tiny-lru@13.0.0: + resolution: {integrity: sha512-xDHxKKS1FdF0Tv2P+QT7IeSEg74K/8cEDzbv3Tv6UyHHUgBOjOiQiBp818MGj66dhurQus/IBcoAbwIKtSGc6Q==} + engines: {node: '>=14'} tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyexec@1.0.4: - resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} engines: {node: '>=18'} tinyglobby@0.2.14: @@ -5828,8 +5873,8 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - ts-jest@29.4.6: - resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} + ts-jest@29.4.9: + resolution: {integrity: sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -5840,7 +5885,7 @@ packages: esbuild: '*' jest: ^29.0.0 || ^30.0.0 jest-util: ^29.0.0 || ^30.0.0 - typescript: '>=4.3 <6' + typescript: '>=4.3 <7' peerDependenciesMeta: '@babel/core': optional: true @@ -6005,6 +6050,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + uuid@13.0.0: resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} hasBin: true @@ -6369,39 +6418,39 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@biomejs/biome@2.4.10': + '@biomejs/biome@2.4.12': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.4.10 - '@biomejs/cli-darwin-x64': 2.4.10 - '@biomejs/cli-linux-arm64': 2.4.10 - '@biomejs/cli-linux-arm64-musl': 2.4.10 - '@biomejs/cli-linux-x64': 2.4.10 - '@biomejs/cli-linux-x64-musl': 2.4.10 - '@biomejs/cli-win32-arm64': 2.4.10 - '@biomejs/cli-win32-x64': 2.4.10 + '@biomejs/cli-darwin-arm64': 2.4.12 + '@biomejs/cli-darwin-x64': 2.4.12 + '@biomejs/cli-linux-arm64': 2.4.12 + '@biomejs/cli-linux-arm64-musl': 2.4.12 + '@biomejs/cli-linux-x64': 2.4.12 + '@biomejs/cli-linux-x64-musl': 2.4.12 + '@biomejs/cli-win32-arm64': 2.4.12 + '@biomejs/cli-win32-x64': 2.4.12 - '@biomejs/cli-darwin-arm64@2.4.10': + '@biomejs/cli-darwin-arm64@2.4.12': optional: true - '@biomejs/cli-darwin-x64@2.4.10': + '@biomejs/cli-darwin-x64@2.4.12': optional: true - '@biomejs/cli-linux-arm64-musl@2.4.10': + '@biomejs/cli-linux-arm64-musl@2.4.12': optional: true - '@biomejs/cli-linux-arm64@2.4.10': + '@biomejs/cli-linux-arm64@2.4.12': optional: true - '@biomejs/cli-linux-x64-musl@2.4.10': + '@biomejs/cli-linux-x64-musl@2.4.12': optional: true - '@biomejs/cli-linux-x64@2.4.10': + '@biomejs/cli-linux-x64@2.4.12': optional: true - '@biomejs/cli-win32-arm64@2.4.10': + '@biomejs/cli-win32-arm64@2.4.12': optional: true - '@biomejs/cli-win32-x64@2.4.10': + '@biomejs/cli-win32-x64@2.4.12': optional: true '@clack/core@0.5.0': @@ -6428,79 +6477,79 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@csstools/postcss-cascade-layers@1.1.1(postcss@8.5.8)': + '@csstools/postcss-cascade-layers@1.1.1(postcss@8.5.10)': dependencies: '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.2) - postcss: 8.5.8 + postcss: 8.5.10 postcss-selector-parser: 6.1.2 - '@csstools/postcss-color-function@1.1.1(postcss@8.5.8)': + '@csstools/postcss-color-function@1.1.1(postcss@8.5.10)': dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.8) - postcss: 8.5.8 + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.10) + postcss: 8.5.10 postcss-value-parser: 4.2.0 - '@csstools/postcss-font-format-keywords@1.0.1(postcss@8.5.8)': + '@csstools/postcss-font-format-keywords@1.0.1(postcss@8.5.10)': dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - '@csstools/postcss-hwb-function@1.0.2(postcss@8.5.8)': + '@csstools/postcss-hwb-function@1.0.2(postcss@8.5.10)': dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - '@csstools/postcss-ic-unit@1.0.1(postcss@8.5.8)': + '@csstools/postcss-ic-unit@1.0.1(postcss@8.5.10)': dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.8) - postcss: 8.5.8 + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.10) + postcss: 8.5.10 postcss-value-parser: 4.2.0 - '@csstools/postcss-is-pseudo-class@2.0.7(postcss@8.5.8)': + '@csstools/postcss-is-pseudo-class@2.0.7(postcss@8.5.10)': dependencies: '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.2) - postcss: 8.5.8 + postcss: 8.5.10 postcss-selector-parser: 6.1.2 - '@csstools/postcss-nested-calc@1.0.0(postcss@8.5.8)': + '@csstools/postcss-nested-calc@1.0.0(postcss@8.5.10)': dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - '@csstools/postcss-normalize-display-values@1.0.1(postcss@8.5.8)': + '@csstools/postcss-normalize-display-values@1.0.1(postcss@8.5.10)': dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - '@csstools/postcss-oklab-function@1.1.1(postcss@8.5.8)': + '@csstools/postcss-oklab-function@1.1.1(postcss@8.5.10)': dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.8) - postcss: 8.5.8 + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.10) + postcss: 8.5.10 postcss-value-parser: 4.2.0 - '@csstools/postcss-progressive-custom-properties@1.3.0(postcss@8.5.8)': + '@csstools/postcss-progressive-custom-properties@1.3.0(postcss@8.5.10)': dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - '@csstools/postcss-stepped-value-functions@1.0.1(postcss@8.5.8)': + '@csstools/postcss-stepped-value-functions@1.0.1(postcss@8.5.10)': dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - '@csstools/postcss-text-decoration-shorthand@1.0.0(postcss@8.5.8)': + '@csstools/postcss-text-decoration-shorthand@1.0.0(postcss@8.5.10)': dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - '@csstools/postcss-trigonometric-functions@1.0.2(postcss@8.5.8)': + '@csstools/postcss-trigonometric-functions@1.0.2(postcss@8.5.10)': dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - '@csstools/postcss-unset-value@1.0.2(postcss@8.5.8)': + '@csstools/postcss-unset-value@1.0.2(postcss@8.5.10)': dependencies: - postcss: 8.5.8 + postcss: 8.5.10 '@csstools/selector-specificity@2.2.0(postcss-selector-parser@6.1.2)': dependencies: @@ -6716,82 +6765,82 @@ snapshots: '@epic-web/invariant@1.0.0': {} - '@esbuild/aix-ppc64@0.27.4': + '@esbuild/aix-ppc64@0.27.7': optional: true - '@esbuild/android-arm64@0.27.4': + '@esbuild/android-arm64@0.27.7': optional: true - '@esbuild/android-arm@0.27.4': + '@esbuild/android-arm@0.27.7': optional: true - '@esbuild/android-x64@0.27.4': + '@esbuild/android-x64@0.27.7': optional: true - '@esbuild/darwin-arm64@0.27.4': + '@esbuild/darwin-arm64@0.27.7': optional: true - '@esbuild/darwin-x64@0.27.4': + '@esbuild/darwin-x64@0.27.7': optional: true - '@esbuild/freebsd-arm64@0.27.4': + '@esbuild/freebsd-arm64@0.27.7': optional: true - '@esbuild/freebsd-x64@0.27.4': + '@esbuild/freebsd-x64@0.27.7': optional: true - '@esbuild/linux-arm64@0.27.4': + '@esbuild/linux-arm64@0.27.7': optional: true - '@esbuild/linux-arm@0.27.4': + '@esbuild/linux-arm@0.27.7': optional: true - '@esbuild/linux-ia32@0.27.4': + '@esbuild/linux-ia32@0.27.7': optional: true - '@esbuild/linux-loong64@0.27.4': + '@esbuild/linux-loong64@0.27.7': optional: true - '@esbuild/linux-mips64el@0.27.4': + '@esbuild/linux-mips64el@0.27.7': optional: true - '@esbuild/linux-ppc64@0.27.4': + '@esbuild/linux-ppc64@0.27.7': optional: true - '@esbuild/linux-riscv64@0.27.4': + '@esbuild/linux-riscv64@0.27.7': optional: true - '@esbuild/linux-s390x@0.27.4': + '@esbuild/linux-s390x@0.27.7': optional: true - '@esbuild/linux-x64@0.27.4': + '@esbuild/linux-x64@0.27.7': optional: true - '@esbuild/netbsd-arm64@0.27.4': + '@esbuild/netbsd-arm64@0.27.7': optional: true - '@esbuild/netbsd-x64@0.27.4': + '@esbuild/netbsd-x64@0.27.7': optional: true - '@esbuild/openbsd-arm64@0.27.4': + '@esbuild/openbsd-arm64@0.27.7': optional: true - '@esbuild/openbsd-x64@0.27.4': + '@esbuild/openbsd-x64@0.27.7': optional: true - '@esbuild/openharmony-arm64@0.27.4': + '@esbuild/openharmony-arm64@0.27.7': optional: true - '@esbuild/sunos-x64@0.27.4': + '@esbuild/sunos-x64@0.27.7': optional: true - '@esbuild/win32-arm64@0.27.4': + '@esbuild/win32-arm64@0.27.7': optional: true - '@esbuild/win32-ia32@0.27.4': + '@esbuild/win32-ia32@0.27.7': optional: true - '@esbuild/win32-x64@0.27.4': + '@esbuild/win32-x64@0.27.7': optional: true '@formatjs/bigdecimal@0.2.0': {} @@ -6980,27 +7029,27 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 24.12.0 + '@types/node': 24.12.2 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3))': + '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 24.12.0 + '@types/node': 24.12.2 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -7027,7 +7076,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 24.12.0 + '@types/node': 24.12.2 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -7049,7 +7098,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 24.12.0 + '@types/node': 24.12.2 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -7067,7 +7116,7 @@ snapshots: '@jest/pattern@30.0.1': dependencies: - '@types/node': 24.12.0 + '@types/node': 24.12.2 jest-regex-util: 30.0.1 '@jest/reporters@29.7.0': @@ -7078,7 +7127,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.30 - '@types/node': 24.12.0 + '@types/node': 24.12.2 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -7152,7 +7201,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 24.12.0 + '@types/node': 24.12.2 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -7162,7 +7211,7 @@ snapshots: '@jest/schemas': 30.0.5 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 24.12.0 + '@types/node': 24.12.2 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -7305,25 +7354,25 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@prisma/adapter-pg@7.6.0': + '@prisma/adapter-pg@7.7.0': dependencies: - '@prisma/driver-adapter-utils': 7.6.0 + '@prisma/driver-adapter-utils': 7.7.0 '@types/pg': 8.20.0 pg: 8.20.0 postgres-array: 3.0.4 transitivePeerDependencies: - pg-native - '@prisma/client-runtime-utils@7.6.0': {} + '@prisma/client-runtime-utils@7.7.0': {} - '@prisma/client@7.6.0(prisma@7.6.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)': + '@prisma/client@7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)': dependencies: - '@prisma/client-runtime-utils': 7.6.0 + '@prisma/client-runtime-utils': 7.7.0 optionalDependencies: - prisma: 7.6.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3) + prisma: 7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3) typescript: 5.9.3 - '@prisma/config@7.6.0': + '@prisma/config@7.7.0': dependencies: c12: 3.1.0 deepmerge-ts: 7.1.5 @@ -7334,7 +7383,7 @@ snapshots: '@prisma/debug@7.2.0': {} - '@prisma/debug@7.6.0': {} + '@prisma/debug@7.7.0': {} '@prisma/dev@0.24.3(typescript@5.9.3)': dependencies: @@ -7358,36 +7407,36 @@ snapshots: transitivePeerDependencies: - typescript - '@prisma/driver-adapter-utils@7.6.0': + '@prisma/driver-adapter-utils@7.7.0': dependencies: - '@prisma/debug': 7.6.0 + '@prisma/debug': 7.7.0 '@prisma/engines-version@7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711': {} - '@prisma/engines@7.6.0': + '@prisma/engines@7.7.0': dependencies: - '@prisma/debug': 7.6.0 + '@prisma/debug': 7.7.0 '@prisma/engines-version': 7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711 - '@prisma/fetch-engine': 7.6.0 - '@prisma/get-platform': 7.6.0 + '@prisma/fetch-engine': 7.7.0 + '@prisma/get-platform': 7.7.0 - '@prisma/extension-read-replicas@0.5.0(@prisma/client@7.6.0(prisma@7.6.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3))': + '@prisma/extension-read-replicas@0.5.0(@prisma/client@7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3))': dependencies: - '@prisma/client': 7.6.0(prisma@7.6.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3) + '@prisma/client': 7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3) - '@prisma/fetch-engine@7.6.0': + '@prisma/fetch-engine@7.7.0': dependencies: - '@prisma/debug': 7.6.0 + '@prisma/debug': 7.7.0 '@prisma/engines-version': 7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711 - '@prisma/get-platform': 7.6.0 + '@prisma/get-platform': 7.7.0 '@prisma/get-platform@7.2.0': dependencies: '@prisma/debug': 7.2.0 - '@prisma/get-platform@7.6.0': + '@prisma/get-platform@7.7.0': dependencies: - '@prisma/debug': 7.6.0 + '@prisma/debug': 7.7.0 '@prisma/query-plan-executor@7.2.0': {} @@ -7931,11 +7980,11 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tanstack/query-core@5.96.0': {} + '@tanstack/query-core@5.99.0': {} - '@tanstack/react-query@5.96.0(react@19.2.5)': + '@tanstack/react-query@5.99.0(react@19.2.5)': dependencies: - '@tanstack/query-core': 5.96.0 + '@tanstack/query-core': 5.99.0 react: 19.2.5 '@trysound/sax@0.2.0': {} @@ -7983,16 +8032,16 @@ snapshots: '@types/fs-extra@8.1.5': dependencies: - '@types/node': 24.12.0 + '@types/node': 24.12.2 '@types/glob@7.2.0': dependencies: '@types/minimatch': 6.0.0 - '@types/node': 24.12.0 + '@types/node': 24.12.2 '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 24.12.0 + '@types/node': 24.12.2 '@types/istanbul-lib-coverage@2.0.6': {} @@ -8015,13 +8064,13 @@ snapshots: dependencies: minimatch: 10.0.3 - '@types/node@24.12.0': + '@types/node@24.12.2': dependencies: undici-types: 7.16.0 '@types/pg@8.20.0': dependencies: - '@types/node': 24.12.0 + '@types/node': 24.12.2 pg-protocol: 1.13.0 pg-types: 2.2.0 @@ -8051,7 +8100,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 24.12.0 + '@types/node': 24.12.2 optional: true '@umami/react-zen@0.245.0(@types/react@19.2.14)(immer@11.1.4)(react-aria-components@1.17.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.1.18)(use-sync-external-store@1.6.0(react@19.2.5))': @@ -8170,14 +8219,14 @@ snapshots: at-least-node@1.0.0: {} - autoprefixer@10.4.21(postcss@8.5.8): + autoprefixer@10.4.21(postcss@8.5.10): dependencies: browserslist: 4.25.2 caniuse-lite: 1.0.30001780 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -8312,16 +8361,16 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - bundle-require@5.1.0(esbuild@0.27.4): + bundle-require@5.1.0(esbuild@0.27.7): dependencies: - esbuild: 0.27.4 + esbuild: 0.27.7 load-tsconfig: 0.2.5 c12@3.1.0: dependencies: chokidar: 4.0.3 confbox: 0.2.4 - defu: 6.1.4 + defu: 6.1.7 dotenv: 16.6.1 exsolve: 1.0.8 giget: 2.0.0 @@ -8412,7 +8461,7 @@ snapshots: dependencies: consola: 3.4.2 - citty@0.2.1: {} + citty@0.2.2: {} cjs-module-lexer@1.4.3: {} @@ -8521,13 +8570,13 @@ snapshots: optionalDependencies: typescript: 5.9.3 - create-jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3)): + create-jest@29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -8557,27 +8606,27 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-blank-pseudo@3.0.3(postcss@8.5.8): + css-blank-pseudo@3.0.3(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-selector-parser: 6.1.2 css-box-model@1.2.1: dependencies: tiny-invariant: 1.3.3 - css-declaration-sorter@6.4.1(postcss@8.5.8): + css-declaration-sorter@6.4.1(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 - css-has-pseudo@3.0.4(postcss@8.5.8): + css-has-pseudo@3.0.4(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-selector-parser: 6.1.2 - css-prefers-color-scheme@6.0.3(postcss@8.5.8): + css-prefers-color-scheme@6.0.3(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 css-select@4.3.0: dependencies: @@ -8616,48 +8665,48 @@ snapshots: cssesc@3.0.0: {} - cssnano-preset-default@5.2.14(postcss@8.5.8): + cssnano-preset-default@5.2.14(postcss@8.5.10): dependencies: - css-declaration-sorter: 6.4.1(postcss@8.5.8) - cssnano-utils: 3.1.0(postcss@8.5.8) - postcss: 8.5.8 - postcss-calc: 8.2.4(postcss@8.5.8) - postcss-colormin: 5.3.1(postcss@8.5.8) - postcss-convert-values: 5.1.3(postcss@8.5.8) - postcss-discard-comments: 5.1.2(postcss@8.5.8) - postcss-discard-duplicates: 5.1.0(postcss@8.5.8) - postcss-discard-empty: 5.1.1(postcss@8.5.8) - postcss-discard-overridden: 5.1.0(postcss@8.5.8) - postcss-merge-longhand: 5.1.7(postcss@8.5.8) - postcss-merge-rules: 5.1.4(postcss@8.5.8) - postcss-minify-font-values: 5.1.0(postcss@8.5.8) - postcss-minify-gradients: 5.1.1(postcss@8.5.8) - postcss-minify-params: 5.1.4(postcss@8.5.8) - postcss-minify-selectors: 5.2.1(postcss@8.5.8) - postcss-normalize-charset: 5.1.0(postcss@8.5.8) - postcss-normalize-display-values: 5.1.0(postcss@8.5.8) - postcss-normalize-positions: 5.1.1(postcss@8.5.8) - postcss-normalize-repeat-style: 5.1.1(postcss@8.5.8) - postcss-normalize-string: 5.1.0(postcss@8.5.8) - postcss-normalize-timing-functions: 5.1.0(postcss@8.5.8) - postcss-normalize-unicode: 5.1.1(postcss@8.5.8) - postcss-normalize-url: 5.1.0(postcss@8.5.8) - postcss-normalize-whitespace: 5.1.1(postcss@8.5.8) - postcss-ordered-values: 5.1.3(postcss@8.5.8) - postcss-reduce-initial: 5.1.2(postcss@8.5.8) - postcss-reduce-transforms: 5.1.0(postcss@8.5.8) - postcss-svgo: 5.1.0(postcss@8.5.8) - postcss-unique-selectors: 5.1.1(postcss@8.5.8) + css-declaration-sorter: 6.4.1(postcss@8.5.10) + cssnano-utils: 3.1.0(postcss@8.5.10) + postcss: 8.5.10 + postcss-calc: 8.2.4(postcss@8.5.10) + postcss-colormin: 5.3.1(postcss@8.5.10) + postcss-convert-values: 5.1.3(postcss@8.5.10) + postcss-discard-comments: 5.1.2(postcss@8.5.10) + postcss-discard-duplicates: 5.1.0(postcss@8.5.10) + postcss-discard-empty: 5.1.1(postcss@8.5.10) + postcss-discard-overridden: 5.1.0(postcss@8.5.10) + postcss-merge-longhand: 5.1.7(postcss@8.5.10) + postcss-merge-rules: 5.1.4(postcss@8.5.10) + postcss-minify-font-values: 5.1.0(postcss@8.5.10) + postcss-minify-gradients: 5.1.1(postcss@8.5.10) + postcss-minify-params: 5.1.4(postcss@8.5.10) + postcss-minify-selectors: 5.2.1(postcss@8.5.10) + postcss-normalize-charset: 5.1.0(postcss@8.5.10) + postcss-normalize-display-values: 5.1.0(postcss@8.5.10) + postcss-normalize-positions: 5.1.1(postcss@8.5.10) + postcss-normalize-repeat-style: 5.1.1(postcss@8.5.10) + postcss-normalize-string: 5.1.0(postcss@8.5.10) + postcss-normalize-timing-functions: 5.1.0(postcss@8.5.10) + postcss-normalize-unicode: 5.1.1(postcss@8.5.10) + postcss-normalize-url: 5.1.0(postcss@8.5.10) + postcss-normalize-whitespace: 5.1.1(postcss@8.5.10) + postcss-ordered-values: 5.1.3(postcss@8.5.10) + postcss-reduce-initial: 5.1.2(postcss@8.5.10) + postcss-reduce-transforms: 5.1.0(postcss@8.5.10) + postcss-svgo: 5.1.0(postcss@8.5.10) + postcss-unique-selectors: 5.1.1(postcss@8.5.10) - cssnano-utils@3.1.0(postcss@8.5.8): + cssnano-utils@3.1.0(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 - cssnano@5.1.15(postcss@8.5.8): + cssnano@5.1.15(postcss@8.5.10): dependencies: - cssnano-preset-default: 5.2.14(postcss@8.5.8) + cssnano-preset-default: 5.2.14(postcss@8.5.10) lilconfig: 2.1.0 - postcss: 8.5.8 + postcss: 8.5.10 yaml: 1.10.2 csso@4.2.0: @@ -8826,7 +8875,7 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - defu@6.1.4: {} + defu@6.1.7: {} del@6.1.1: dependencies: @@ -8913,7 +8962,7 @@ snapshots: dotenv-cli@11.0.0: dependencies: cross-spawn: 7.0.6 - dotenv: 17.3.1 + dotenv: 17.4.2 dotenv-expand: 12.0.3 minimist: 1.2.8 @@ -8923,7 +8972,7 @@ snapshots: dotenv@16.6.1: {} - dotenv@17.3.1: {} + dotenv@17.4.2: {} dunder-proto@1.0.1: dependencies: @@ -9054,34 +9103,34 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild@0.27.4: + esbuild@0.27.7: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.4 - '@esbuild/android-arm': 0.27.4 - '@esbuild/android-arm64': 0.27.4 - '@esbuild/android-x64': 0.27.4 - '@esbuild/darwin-arm64': 0.27.4 - '@esbuild/darwin-x64': 0.27.4 - '@esbuild/freebsd-arm64': 0.27.4 - '@esbuild/freebsd-x64': 0.27.4 - '@esbuild/linux-arm': 0.27.4 - '@esbuild/linux-arm64': 0.27.4 - '@esbuild/linux-ia32': 0.27.4 - '@esbuild/linux-loong64': 0.27.4 - '@esbuild/linux-mips64el': 0.27.4 - '@esbuild/linux-ppc64': 0.27.4 - '@esbuild/linux-riscv64': 0.27.4 - '@esbuild/linux-s390x': 0.27.4 - '@esbuild/linux-x64': 0.27.4 - '@esbuild/netbsd-arm64': 0.27.4 - '@esbuild/netbsd-x64': 0.27.4 - '@esbuild/openbsd-arm64': 0.27.4 - '@esbuild/openbsd-x64': 0.27.4 - '@esbuild/openharmony-arm64': 0.27.4 - '@esbuild/sunos-x64': 0.27.4 - '@esbuild/win32-arm64': 0.27.4 - '@esbuild/win32-ia32': 0.27.4 - '@esbuild/win32-x64': 0.27.4 + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 escalade@3.2.0: {} @@ -9342,7 +9391,7 @@ snapshots: dependencies: citty: 0.1.6 consola: 3.4.2 - defu: 6.1.4 + defu: 6.1.7 node-fetch-native: 1.6.7 nypm: 0.6.5 pathe: 2.0.3 @@ -9423,7 +9472,7 @@ snapshots: graphmatch@1.1.1: {} - handlebars@4.7.8: + handlebars@4.7.9: dependencies: minimist: 1.2.8 neo-async: 2.6.2 @@ -9480,9 +9529,9 @@ snapshots: icss-replace-symbols@1.1.0: {} - icss-utils@5.1.0(postcss@8.5.8): + icss-utils@5.1.0(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 icu-minify@4.8.4: dependencies: @@ -9701,7 +9750,7 @@ snapshots: isarray@2.0.5: {} - isbot@5.1.37: {} + isbot@5.1.39: {} isexe@2.0.0: {} @@ -9766,7 +9815,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 24.12.0 + '@types/node': 24.12.2 chalk: 4.1.2 co: 4.6.0 dedent: 1.6.0 @@ -9786,16 +9835,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3)): + jest-cli@29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3)) + create-jest: 29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -9805,7 +9854,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3)): + jest-config@29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.3 '@jest/test-sequencer': 29.7.0 @@ -9830,8 +9879,8 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 24.12.0 - ts-node: 10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3) + '@types/node': 24.12.2 + ts-node: 10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -9867,7 +9916,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 24.12.0 + '@types/node': 24.12.2 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -9877,7 +9926,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 24.12.0 + '@types/node': 24.12.2 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -9935,13 +9984,13 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 24.12.0 + '@types/node': 24.12.2 jest-util: 29.7.0 jest-mock@30.0.5: dependencies: '@jest/types': 30.0.5 - '@types/node': 24.12.0 + '@types/node': 24.12.2 jest-util: 30.0.5 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -9978,7 +10027,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 24.12.0 + '@types/node': 24.12.2 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -10006,7 +10055,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 24.12.0 + '@types/node': 24.12.2 chalk: 4.1.2 cjs-module-lexer: 1.4.3 collect-v8-coverage: 1.0.2 @@ -10052,7 +10101,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 24.12.0 + '@types/node': 24.12.2 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -10061,7 +10110,7 @@ snapshots: jest-util@30.0.5: dependencies: '@jest/types': 30.0.5 - '@types/node': 24.12.0 + '@types/node': 24.12.2 chalk: 4.1.2 ci-info: 4.3.0 graceful-fs: 4.2.11 @@ -10080,7 +10129,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 24.12.0 + '@types/node': 24.12.2 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -10089,17 +10138,17 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 24.12.0 + '@types/node': 24.12.2 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3)): + jest@29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3)) + jest-cli: 29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -10283,11 +10332,15 @@ snapshots: lru.min@1.1.4: {} + lucide-react@0.542.0(react@19.2.5): + dependencies: + react: 19.2.5 + lucide-react@0.555.0(react@19.2.5): dependencies: react: 19.2.5 - lucide-react@1.7.0(react@19.2.5): + lucide-react@1.8.0(react@19.2.5): dependencies: react: 19.2.5 @@ -10311,10 +10364,10 @@ snapshots: math-intrinsics@1.1.0: {} - maxmind@5.0.5: + maxmind@5.0.6: dependencies: mmdb-lib: 3.0.2 - tiny-lru: 11.4.7 + tiny-lru: 13.0.0 mdn-data@2.0.14: {} @@ -10514,9 +10567,9 @@ snapshots: nypm@0.6.5: dependencies: - citty: 0.2.1 + citty: 0.2.2 pathe: 2.0.3 - tinyexec: 1.0.4 + tinyexec: 1.1.1 object-assign@4.1.1: {} @@ -10708,390 +10761,390 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-attribute-case-insensitive@5.0.2(postcss@8.5.8): + postcss-attribute-case-insensitive@5.0.2(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-selector-parser: 6.1.2 - postcss-calc@8.2.4(postcss@8.5.8): + postcss-calc@8.2.4(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-selector-parser: 6.1.2 postcss-value-parser: 4.2.0 - postcss-clamp@4.1.0(postcss@8.5.8): + postcss-clamp@4.1.0(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-color-functional-notation@4.2.4(postcss@8.5.8): + postcss-color-functional-notation@4.2.4(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-color-hex-alpha@8.0.4(postcss@8.5.8): + postcss-color-hex-alpha@8.0.4(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-color-rebeccapurple@7.1.1(postcss@8.5.8): + postcss-color-rebeccapurple@7.1.1(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-colormin@5.3.1(postcss@8.5.8): + postcss-colormin@5.3.1(postcss@8.5.10): dependencies: browserslist: 4.25.2 caniuse-api: 3.0.0 colord: 2.9.3 - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-convert-values@5.1.3(postcss@8.5.8): + postcss-convert-values@5.1.3(postcss@8.5.10): dependencies: browserslist: 4.25.2 - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-custom-media@8.0.2(postcss@8.5.8): + postcss-custom-media@8.0.2(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-custom-properties@12.1.11(postcss@8.5.8): + postcss-custom-properties@12.1.11(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-custom-selectors@6.0.3(postcss@8.5.8): + postcss-custom-selectors@6.0.3(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-selector-parser: 6.1.2 - postcss-dir-pseudo-class@6.0.5(postcss@8.5.8): + postcss-dir-pseudo-class@6.0.5(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-selector-parser: 6.1.2 - postcss-discard-comments@5.1.2(postcss@8.5.8): + postcss-discard-comments@5.1.2(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 - postcss-discard-duplicates@5.1.0(postcss@8.5.8): + postcss-discard-duplicates@5.1.0(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 - postcss-discard-empty@5.1.1(postcss@8.5.8): + postcss-discard-empty@5.1.1(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 - postcss-discard-overridden@5.1.0(postcss@8.5.8): + postcss-discard-overridden@5.1.0(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 - postcss-double-position-gradients@3.1.2(postcss@8.5.8): + postcss-double-position-gradients@3.1.2(postcss@8.5.10): dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.8) - postcss: 8.5.8 + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.10) + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-env-function@4.0.6(postcss@8.5.8): + postcss-env-function@4.0.6(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-flexbugs-fixes@5.0.2(postcss@8.5.8): + postcss-flexbugs-fixes@5.0.2(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 - postcss-focus-visible@6.0.4(postcss@8.5.8): + postcss-focus-visible@6.0.4(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-selector-parser: 6.1.2 - postcss-focus-within@5.0.4(postcss@8.5.8): + postcss-focus-within@5.0.4(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-selector-parser: 6.1.2 - postcss-font-variant@5.0.0(postcss@8.5.8): + postcss-font-variant@5.0.0(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 - postcss-gap-properties@3.0.5(postcss@8.5.8): + postcss-gap-properties@3.0.5(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 - postcss-image-set-function@4.0.7(postcss@8.5.8): + postcss-image-set-function@4.0.7(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-import@15.1.0(postcss@8.5.8): + postcss-import@15.1.0(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.10 - postcss-initial@4.0.1(postcss@8.5.8): + postcss-initial@4.0.1(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 - postcss-lab-function@4.2.1(postcss@8.5.8): + postcss-lab-function@4.2.1(postcss@8.5.10): dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.8) - postcss: 8.5.8 + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.10) + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-load-config@3.1.4(postcss@8.5.8)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3)): + postcss-load-config@3.1.4(postcss@8.5.10)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)): dependencies: lilconfig: 2.1.0 yaml: 1.10.2 optionalDependencies: - postcss: 8.5.8 - ts-node: 10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3) + postcss: 8.5.10 + ts-node: 10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3) - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.1): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(yaml@2.8.1): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.6.1 - postcss: 8.5.8 + postcss: 8.5.10 tsx: 4.21.0 yaml: 2.8.1 - postcss-logical@5.0.4(postcss@8.5.8): + postcss-logical@5.0.4(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 - postcss-media-minmax@5.0.0(postcss@8.5.8): + postcss-media-minmax@5.0.0(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 - postcss-merge-longhand@5.1.7(postcss@8.5.8): + postcss-merge-longhand@5.1.7(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - stylehacks: 5.1.1(postcss@8.5.8) + stylehacks: 5.1.1(postcss@8.5.10) - postcss-merge-rules@5.1.4(postcss@8.5.8): + postcss-merge-rules@5.1.4(postcss@8.5.10): dependencies: browserslist: 4.25.2 caniuse-api: 3.0.0 - cssnano-utils: 3.1.0(postcss@8.5.8) - postcss: 8.5.8 + cssnano-utils: 3.1.0(postcss@8.5.10) + postcss: 8.5.10 postcss-selector-parser: 6.1.2 - postcss-minify-font-values@5.1.0(postcss@8.5.8): + postcss-minify-font-values@5.1.0(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-minify-gradients@5.1.1(postcss@8.5.8): + postcss-minify-gradients@5.1.1(postcss@8.5.10): dependencies: colord: 2.9.3 - cssnano-utils: 3.1.0(postcss@8.5.8) - postcss: 8.5.8 + cssnano-utils: 3.1.0(postcss@8.5.10) + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-minify-params@5.1.4(postcss@8.5.8): + postcss-minify-params@5.1.4(postcss@8.5.10): dependencies: browserslist: 4.25.2 - cssnano-utils: 3.1.0(postcss@8.5.8) - postcss: 8.5.8 + cssnano-utils: 3.1.0(postcss@8.5.10) + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-minify-selectors@5.2.1(postcss@8.5.8): + postcss-minify-selectors@5.2.1(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-selector-parser: 6.1.2 - postcss-modules-extract-imports@3.1.0(postcss@8.5.8): + postcss-modules-extract-imports@3.1.0(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 - postcss-modules-local-by-default@4.2.0(postcss@8.5.8): + postcss-modules-local-by-default@4.2.0(postcss@8.5.10): dependencies: - icss-utils: 5.1.0(postcss@8.5.8) - postcss: 8.5.8 + icss-utils: 5.1.0(postcss@8.5.10) + postcss: 8.5.10 postcss-selector-parser: 7.1.0 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.1(postcss@8.5.8): + postcss-modules-scope@3.2.1(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-selector-parser: 7.1.0 - postcss-modules-values@4.0.0(postcss@8.5.8): + postcss-modules-values@4.0.0(postcss@8.5.10): dependencies: - icss-utils: 5.1.0(postcss@8.5.8) - postcss: 8.5.8 + icss-utils: 5.1.0(postcss@8.5.10) + postcss: 8.5.10 - postcss-modules@4.3.1(postcss@8.5.8): + postcss-modules@4.3.1(postcss@8.5.10): dependencies: generic-names: 4.0.0 icss-replace-symbols: 1.1.0 lodash.camelcase: 4.3.0 - postcss: 8.5.8 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.8) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.8) - postcss-modules-scope: 3.2.1(postcss@8.5.8) - postcss-modules-values: 4.0.0(postcss@8.5.8) + postcss: 8.5.10 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.10) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.10) + postcss-modules-scope: 3.2.1(postcss@8.5.10) + postcss-modules-values: 4.0.0(postcss@8.5.10) string-hash: 1.1.3 - postcss-nesting@10.2.0(postcss@8.5.8): + postcss-nesting@10.2.0(postcss@8.5.10): dependencies: '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.2) - postcss: 8.5.8 + postcss: 8.5.10 postcss-selector-parser: 6.1.2 - postcss-normalize-charset@5.1.0(postcss@8.5.8): + postcss-normalize-charset@5.1.0(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 - postcss-normalize-display-values@5.1.0(postcss@8.5.8): + postcss-normalize-display-values@5.1.0(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-normalize-positions@5.1.1(postcss@8.5.8): + postcss-normalize-positions@5.1.1(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@5.1.1(postcss@8.5.8): + postcss-normalize-repeat-style@5.1.1(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-normalize-string@5.1.0(postcss@8.5.8): + postcss-normalize-string@5.1.0(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@5.1.0(postcss@8.5.8): + postcss-normalize-timing-functions@5.1.0(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@5.1.1(postcss@8.5.8): + postcss-normalize-unicode@5.1.1(postcss@8.5.10): dependencies: browserslist: 4.25.2 - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-normalize-url@5.1.0(postcss@8.5.8): + postcss-normalize-url@5.1.0(postcss@8.5.10): dependencies: normalize-url: 6.1.0 - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@5.1.1(postcss@8.5.8): + postcss-normalize-whitespace@5.1.1(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-opacity-percentage@1.1.3(postcss@8.5.8): + postcss-opacity-percentage@1.1.3(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 - postcss-ordered-values@5.1.3(postcss@8.5.8): + postcss-ordered-values@5.1.3(postcss@8.5.10): dependencies: - cssnano-utils: 3.1.0(postcss@8.5.8) - postcss: 8.5.8 + cssnano-utils: 3.1.0(postcss@8.5.10) + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-overflow-shorthand@3.0.4(postcss@8.5.8): + postcss-overflow-shorthand@3.0.4(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-page-break@3.0.4(postcss@8.5.8): + postcss-page-break@3.0.4(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 - postcss-place@7.0.5(postcss@8.5.8): + postcss-place@7.0.5(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-preset-env@7.8.3(postcss@8.5.8): + postcss-preset-env@7.8.3(postcss@8.5.10): dependencies: - '@csstools/postcss-cascade-layers': 1.1.1(postcss@8.5.8) - '@csstools/postcss-color-function': 1.1.1(postcss@8.5.8) - '@csstools/postcss-font-format-keywords': 1.0.1(postcss@8.5.8) - '@csstools/postcss-hwb-function': 1.0.2(postcss@8.5.8) - '@csstools/postcss-ic-unit': 1.0.1(postcss@8.5.8) - '@csstools/postcss-is-pseudo-class': 2.0.7(postcss@8.5.8) - '@csstools/postcss-nested-calc': 1.0.0(postcss@8.5.8) - '@csstools/postcss-normalize-display-values': 1.0.1(postcss@8.5.8) - '@csstools/postcss-oklab-function': 1.1.1(postcss@8.5.8) - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.8) - '@csstools/postcss-stepped-value-functions': 1.0.1(postcss@8.5.8) - '@csstools/postcss-text-decoration-shorthand': 1.0.0(postcss@8.5.8) - '@csstools/postcss-trigonometric-functions': 1.0.2(postcss@8.5.8) - '@csstools/postcss-unset-value': 1.0.2(postcss@8.5.8) - autoprefixer: 10.4.21(postcss@8.5.8) + '@csstools/postcss-cascade-layers': 1.1.1(postcss@8.5.10) + '@csstools/postcss-color-function': 1.1.1(postcss@8.5.10) + '@csstools/postcss-font-format-keywords': 1.0.1(postcss@8.5.10) + '@csstools/postcss-hwb-function': 1.0.2(postcss@8.5.10) + '@csstools/postcss-ic-unit': 1.0.1(postcss@8.5.10) + '@csstools/postcss-is-pseudo-class': 2.0.7(postcss@8.5.10) + '@csstools/postcss-nested-calc': 1.0.0(postcss@8.5.10) + '@csstools/postcss-normalize-display-values': 1.0.1(postcss@8.5.10) + '@csstools/postcss-oklab-function': 1.1.1(postcss@8.5.10) + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.10) + '@csstools/postcss-stepped-value-functions': 1.0.1(postcss@8.5.10) + '@csstools/postcss-text-decoration-shorthand': 1.0.0(postcss@8.5.10) + '@csstools/postcss-trigonometric-functions': 1.0.2(postcss@8.5.10) + '@csstools/postcss-unset-value': 1.0.2(postcss@8.5.10) + autoprefixer: 10.4.21(postcss@8.5.10) browserslist: 4.25.2 - css-blank-pseudo: 3.0.3(postcss@8.5.8) - css-has-pseudo: 3.0.4(postcss@8.5.8) - css-prefers-color-scheme: 6.0.3(postcss@8.5.8) + css-blank-pseudo: 3.0.3(postcss@8.5.10) + css-has-pseudo: 3.0.4(postcss@8.5.10) + css-prefers-color-scheme: 6.0.3(postcss@8.5.10) cssdb: 7.11.2 - postcss: 8.5.8 - postcss-attribute-case-insensitive: 5.0.2(postcss@8.5.8) - postcss-clamp: 4.1.0(postcss@8.5.8) - postcss-color-functional-notation: 4.2.4(postcss@8.5.8) - postcss-color-hex-alpha: 8.0.4(postcss@8.5.8) - postcss-color-rebeccapurple: 7.1.1(postcss@8.5.8) - postcss-custom-media: 8.0.2(postcss@8.5.8) - postcss-custom-properties: 12.1.11(postcss@8.5.8) - postcss-custom-selectors: 6.0.3(postcss@8.5.8) - postcss-dir-pseudo-class: 6.0.5(postcss@8.5.8) - postcss-double-position-gradients: 3.1.2(postcss@8.5.8) - postcss-env-function: 4.0.6(postcss@8.5.8) - postcss-focus-visible: 6.0.4(postcss@8.5.8) - postcss-focus-within: 5.0.4(postcss@8.5.8) - postcss-font-variant: 5.0.0(postcss@8.5.8) - postcss-gap-properties: 3.0.5(postcss@8.5.8) - postcss-image-set-function: 4.0.7(postcss@8.5.8) - postcss-initial: 4.0.1(postcss@8.5.8) - postcss-lab-function: 4.2.1(postcss@8.5.8) - postcss-logical: 5.0.4(postcss@8.5.8) - postcss-media-minmax: 5.0.0(postcss@8.5.8) - postcss-nesting: 10.2.0(postcss@8.5.8) - postcss-opacity-percentage: 1.1.3(postcss@8.5.8) - postcss-overflow-shorthand: 3.0.4(postcss@8.5.8) - postcss-page-break: 3.0.4(postcss@8.5.8) - postcss-place: 7.0.5(postcss@8.5.8) - postcss-pseudo-class-any-link: 7.1.6(postcss@8.5.8) - postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.8) - postcss-selector-not: 6.0.1(postcss@8.5.8) + postcss: 8.5.10 + postcss-attribute-case-insensitive: 5.0.2(postcss@8.5.10) + postcss-clamp: 4.1.0(postcss@8.5.10) + postcss-color-functional-notation: 4.2.4(postcss@8.5.10) + postcss-color-hex-alpha: 8.0.4(postcss@8.5.10) + postcss-color-rebeccapurple: 7.1.1(postcss@8.5.10) + postcss-custom-media: 8.0.2(postcss@8.5.10) + postcss-custom-properties: 12.1.11(postcss@8.5.10) + postcss-custom-selectors: 6.0.3(postcss@8.5.10) + postcss-dir-pseudo-class: 6.0.5(postcss@8.5.10) + postcss-double-position-gradients: 3.1.2(postcss@8.5.10) + postcss-env-function: 4.0.6(postcss@8.5.10) + postcss-focus-visible: 6.0.4(postcss@8.5.10) + postcss-focus-within: 5.0.4(postcss@8.5.10) + postcss-font-variant: 5.0.0(postcss@8.5.10) + postcss-gap-properties: 3.0.5(postcss@8.5.10) + postcss-image-set-function: 4.0.7(postcss@8.5.10) + postcss-initial: 4.0.1(postcss@8.5.10) + postcss-lab-function: 4.2.1(postcss@8.5.10) + postcss-logical: 5.0.4(postcss@8.5.10) + postcss-media-minmax: 5.0.0(postcss@8.5.10) + postcss-nesting: 10.2.0(postcss@8.5.10) + postcss-opacity-percentage: 1.1.3(postcss@8.5.10) + postcss-overflow-shorthand: 3.0.4(postcss@8.5.10) + postcss-page-break: 3.0.4(postcss@8.5.10) + postcss-place: 7.0.5(postcss@8.5.10) + postcss-pseudo-class-any-link: 7.1.6(postcss@8.5.10) + postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.10) + postcss-selector-not: 6.0.1(postcss@8.5.10) postcss-value-parser: 4.2.0 - postcss-pseudo-class-any-link@7.1.6(postcss@8.5.8): + postcss-pseudo-class-any-link@7.1.6(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-selector-parser: 6.1.2 - postcss-reduce-initial@5.1.2(postcss@8.5.8): + postcss-reduce-initial@5.1.2(postcss@8.5.10): dependencies: browserslist: 4.25.2 caniuse-api: 3.0.0 - postcss: 8.5.8 + postcss: 8.5.10 - postcss-reduce-transforms@5.1.0(postcss@8.5.8): + postcss-reduce-transforms@5.1.0(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-replace-overflow-wrap@4.0.0(postcss@8.5.8): + postcss-replace-overflow-wrap@4.0.0(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 - postcss-selector-not@6.0.1(postcss@8.5.8): + postcss-selector-not@6.0.1(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.1.2: @@ -11104,15 +11157,15 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-svgo@5.1.0(postcss@8.5.8): + postcss-svgo@5.1.0(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-value-parser: 4.2.0 svgo: 2.8.0 - postcss-unique-selectors@5.1.1(postcss@8.5.8): + postcss-unique-selectors@5.1.1(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-selector-parser: 6.1.2 postcss-value-parser@4.2.0: {} @@ -11123,7 +11176,7 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.8: + postcss@8.5.10: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -11161,11 +11214,11 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - prisma@7.6.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3): + prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3): dependencies: - '@prisma/config': 7.6.0 + '@prisma/config': 7.7.0 '@prisma/dev': 0.24.3(typescript@5.9.3) - '@prisma/engines': 7.6.0 + '@prisma/engines': 7.7.0 '@prisma/studio-core': 0.27.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) mysql2: 3.15.3 postgres: 3.4.7 @@ -11226,7 +11279,7 @@ snapshots: rc9@2.1.2: dependencies: - defu: 6.1.4 + defu: 6.1.7 destr: 2.0.5 react-aria-components@1.17.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): @@ -11281,7 +11334,7 @@ snapshots: '@types/react': 19.2.14 redux: 5.0.1 - react-resizable-panels@4.8.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + react-resizable-panels@4.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -11451,17 +11504,17 @@ snapshots: dependencies: rollup: 4.60.1 - rollup-plugin-postcss@4.0.2(postcss@8.5.8)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3)): + rollup-plugin-postcss@4.0.2(postcss@8.5.10)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)): dependencies: chalk: 4.1.2 concat-with-sourcemaps: 1.1.0 - cssnano: 5.1.15(postcss@8.5.8) + cssnano: 5.1.15(postcss@8.5.10) import-cwd: 3.0.0 p-queue: 6.6.2 pify: 5.0.0 - postcss: 8.5.8 - postcss-load-config: 3.1.4(postcss@8.5.8)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3)) - postcss-modules: 4.3.1(postcss@8.5.8) + postcss: 8.5.10 + postcss-load-config: 3.1.4(postcss@8.5.10)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) + postcss-modules: 4.3.1(postcss@8.5.10) promise.series: 0.2.0 resolve: 1.22.10 rollup-pluginutils: 2.8.2 @@ -11846,10 +11899,10 @@ snapshots: optionalDependencies: '@babel/core': 7.28.3 - stylehacks@5.1.1(postcss@8.5.8): + stylehacks@5.1.1(postcss@8.5.10): dependencies: browserslist: 4.25.2 - postcss: 8.5.8 + postcss: 8.5.10 postcss-selector-parser: 6.1.2 sucrase@3.35.0: @@ -11931,6 +11984,8 @@ snapshots: thenby@1.3.4: {} + thenby@1.4.0: {} + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -11945,11 +12000,11 @@ snapshots: tiny-invariant@1.3.3: {} - tiny-lru@11.4.7: {} + tiny-lru@13.0.0: {} tinyexec@0.3.2: {} - tinyexec@1.0.4: {} + tinyexec@1.1.1: {} tinyglobby@0.2.14: dependencies: @@ -11982,12 +12037,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.6(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.27.4)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.9(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.27.7)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.8 - jest: 29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3)) + handlebars: 4.7.9 + jest: 29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -12000,7 +12055,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 30.0.5 babel-jest: 29.7.0(@babel/core@7.28.3) - esbuild: 0.27.4 + esbuild: 0.27.7 jest-util: 30.0.5 ts-morph@27.0.2: @@ -12008,14 +12063,14 @@ snapshots: '@ts-morph/common': 0.28.1 code-block-writer: 13.0.3 - ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.0)(typescript@5.9.3): + ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 24.12.0 + '@types/node': 24.12.2 acorn: 8.15.0 acorn-walk: 8.3.4 arg: 4.1.3 @@ -12030,18 +12085,18 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(@swc/core@1.15.11(@swc/helpers@0.5.21))(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1): + tsup@8.5.1(@swc/core@1.15.11(@swc/helpers@0.5.21))(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1): dependencies: - bundle-require: 5.1.0(esbuild@0.27.4) + bundle-require: 5.1.0(esbuild@0.27.7) cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.2 debug: 4.4.3(supports-color@8.1.1) - esbuild: 0.27.4 + esbuild: 0.27.7 fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.1) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(yaml@2.8.1) resolve-from: 5.0.0 rollup: 4.60.1 source-map: 0.7.6 @@ -12051,7 +12106,7 @@ snapshots: tree-kill: 1.2.2 optionalDependencies: '@swc/core': 1.15.11(@swc/helpers@0.5.21) - postcss: 8.5.8 + postcss: 8.5.10 typescript: 5.9.3 transitivePeerDependencies: - jiti @@ -12061,7 +12116,7 @@ snapshots: tsx@4.21.0: dependencies: - esbuild: 0.27.4 + esbuild: 0.27.7 get-tsconfig: 4.13.0 optionalDependencies: fsevents: 2.3.3 @@ -12171,6 +12226,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@11.1.0: {} + uuid@13.0.0: {} uuid@8.3.2: {} From b822374faa609b32446fd8146e97c71b4010aedf Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 17 Apr 2026 15:57:38 -0700 Subject: [PATCH 06/59] Updated major packages. --- package.json | 58 +- pnpm-lock.yaml | 4383 ++++++++++++++++++++++++++++++------------------ 2 files changed, 2818 insertions(+), 1623 deletions(-) diff --git a/package.json b/package.json index d04c2172f..ec6e3fed8 100644 --- a/package.json +++ b/package.json @@ -52,10 +52,10 @@ ], "dependencies": { "@clickhouse/client": "^1.18.2", - "@date-fns/utc": "^1.2.0", + "@date-fns/utc": "^2.1.1", "@dicebear/collection": "^9.4.2", "@dicebear/core": "^9.4.2", - "@hello-pangea/dnd": "^17.0.0", + "@hello-pangea/dnd": "^18.0.1", "@prisma/adapter-pg": "^7.7.0", "@prisma/client": "^7.7.0", "@prisma/extension-read-replicas": "^0.5.0", @@ -71,18 +71,18 @@ "colord": "^2.9.2", "cors": "^2.8.6", "cross-spawn": "^7.0.3", - "date-fns": "^2.23.0", - "date-fns-tz": "^1.1.4", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "debug": "^4.4.3", - "del": "^6.0.0", + "del": "^8.0.1", "detect-browser": "^5.2.0", "dotenv": "^17.4.2", - "esbuild": "^0.27.7", + "esbuild": "^0.28.0", "immer": "^11.1.4", "ipaddr.js": "^2.3.0", - "is-ci": "^3.0.1", - "is-docker": "^3.0.0", - "is-localhost-ip": "^2.0.0", + "is-ci": "^4.1.0", + "is-docker": "^4.0.0", + "is-localhost-ip": "^3.0.1", "isbot": "^5.1.39", "jsonwebtoken": "^9.0.3", "jszip": "^3.10.1", @@ -90,26 +90,26 @@ "lucide-react": "^1.8.0", "maxmind": "^5.0.6", "next": "16.2.4", - "next-intl": "4.8.3", + "next-intl": "4.9.1", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "papaparse": "^5.5.3", "pg": "^8.20.0", "prisma": "^7.7.0", - "pure-rand": "^7.0.1", + "pure-rand": "^8.4.0", "react": "^19.2.5", "react-dom": "^19.2.5", - "react-error-boundary": "^4.0.4", + "react-error-boundary": "^6.1.1", "react-resizable-panels": "^4.10.0", - "react-simple-maps": "^2.3.0", + "react-simple-maps": "^3.0.0", "react-use-measure": "^2.0.4", - "react-window": "^1.8.6", - "redis": "^4.5.1", + "react-window": "^2.2.7", + "redis": "^5.12.1", "request-ip": "^3.3.0", "rrweb": "2.0.0-alpha.4", "rrweb-player": "1.0.0-alpha.4", "semver": "^7.7.4", - "serialize-error": "^12.0.0", + "serialize-error": "^13.0.1", "thenby": "^1.4.0", "ua-parser-js": "^2.0.9", "uuid": "^13.0.0", @@ -119,40 +119,40 @@ "devDependencies": { "@biomejs/biome": "^2.4.12", "@netlify/plugin-nextjs": "^5.15.9", - "@rollup/plugin-alias": "^5.0.0", - "@rollup/plugin-commonjs": "^25.0.4", + "@rollup/plugin-alias": "^6.0.0", + "@rollup/plugin-commonjs": "^29.0.2", "@rollup/plugin-json": "^6.0.0", - "@rollup/plugin-node-resolve": "^15.2.0", - "@rollup/plugin-replace": "^5.0.2", - "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-node-resolve": "^16.0.3", + "@rollup/plugin-replace": "^6.0.3", + "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.3.0", "@types/jest": "^30.0.0", - "@types/node": "^24.12.2", + "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.2", "babel-plugin-react-compiler": "19.1.0-rc.2", "cross-env": "^10.1.0", - "cypress": "^13.6.6", + "cypress": "^15.14.0", "dotenv-cli": "^11.0.0", - "jest": "^29.7.0", + "jest": "^30.3.0", "postcss": "^8.5.10", "postcss-flexbugs-fixes": "^5.0.2", - "postcss-import": "^15.1.0", - "postcss-preset-env": "7.8.3", + "postcss-import": "^16.1.1", + "postcss-preset-env": "11.2.1", "prompts": "2.4.2", "rollup": "^4.60.1", "rollup-plugin-copy": "^3.4.0", "rollup-plugin-delete": "^3.0.2", "rollup-plugin-dts": "^6.4.1", - "rollup-plugin-node-externals": "^8.1.1", + "rollup-plugin-node-externals": "^9.0.1", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", "tar": "^7.5.13", "ts-jest": "^29.4.9", - "ts-morph": "^27.0.2", + "ts-morph": "^28.0.0", "ts-node": "^10.9.1", "tsup": "^8.5.0", "tsx": "^4.19.0", - "typescript": "^5.9.3" + "typescript": "^6.0.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 767b812d1..ff28acadc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^1.18.2 version: 1.18.2 '@date-fns/utc': - specifier: ^1.2.0 - version: 1.2.0 + specifier: ^2.1.1 + version: 2.1.1 '@dicebear/collection': specifier: ^9.4.2 version: 9.4.2(@dicebear/core@9.4.2) @@ -21,23 +21,23 @@ importers: specifier: ^9.4.2 version: 9.4.2 '@hello-pangea/dnd': - specifier: ^17.0.0 - version: 17.0.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: ^18.0.1 + version: 18.0.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@prisma/adapter-pg': specifier: ^7.7.0 version: 7.7.0 '@prisma/client': specifier: ^7.7.0 - version: 7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3) + version: 7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3))(typescript@6.0.3) '@prisma/extension-read-replicas': specifier: ^0.5.0 - version: 0.5.0(@prisma/client@7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)) + version: 0.5.0(@prisma/client@7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3))(typescript@6.0.3)) '@react-spring/web': specifier: ^10.0.3 version: 10.0.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@svgr/cli': specifier: ^8.1.0 - version: 8.1.0(typescript@5.9.3) + version: 8.1.0(typescript@6.0.3) '@tanstack/react-query': specifier: ^5.99.0 version: 5.99.0(react@19.2.5) @@ -55,7 +55,7 @@ importers: version: 4.5.1 chartjs-adapter-date-fns: specifier: ^3.0.0 - version: 3.0.0(chart.js@4.5.1)(date-fns@2.30.0) + version: 3.0.0(chart.js@4.5.1)(date-fns@4.1.0) classnames: specifier: ^2.3.1 version: 2.5.1 @@ -69,17 +69,17 @@ importers: specifier: ^7.0.3 version: 7.0.6 date-fns: - specifier: ^2.23.0 - version: 2.30.0 + specifier: ^4.1.0 + version: 4.1.0 date-fns-tz: - specifier: ^1.1.4 - version: 1.3.8(date-fns@2.30.0) + specifier: ^3.2.0 + version: 3.2.0(date-fns@4.1.0) debug: specifier: ^4.4.3 version: 4.4.3(supports-color@8.1.1) del: - specifier: ^6.0.0 - version: 6.1.1 + specifier: ^8.0.1 + version: 8.0.1 detect-browser: specifier: ^5.2.0 version: 5.3.0 @@ -87,8 +87,8 @@ importers: specifier: ^17.4.2 version: 17.4.2 esbuild: - specifier: ^0.27.7 - version: 0.27.7 + specifier: ^0.28.0 + version: 0.28.0 immer: specifier: ^11.1.4 version: 11.1.4 @@ -96,14 +96,14 @@ importers: specifier: ^2.3.0 version: 2.3.0 is-ci: + specifier: ^4.1.0 + version: 4.1.0 + is-docker: + specifier: ^4.0.0 + version: 4.0.0 + is-localhost-ip: specifier: ^3.0.1 version: 3.0.1 - is-docker: - specifier: ^3.0.0 - version: 3.0.0 - is-localhost-ip: - specifier: ^2.0.0 - version: 2.0.0 isbot: specifier: ^5.1.39 version: 5.1.39 @@ -124,10 +124,10 @@ importers: version: 5.0.6 next: specifier: 16.2.4 - version: 16.2.4(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 16.2.4(@babel/core@7.29.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) next-intl: - specifier: 4.8.3 - version: 4.8.3(@swc/helpers@0.5.21)(next@16.2.4(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@5.9.3) + specifier: 4.9.1 + version: 4.9.1(@swc/helpers@0.5.21)(next@16.2.4(@babel/core@7.29.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@6.0.3) node-fetch: specifier: ^3.2.8 version: 3.3.2 @@ -142,10 +142,10 @@ importers: version: 8.20.0 prisma: specifier: ^7.7.0 - version: 7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3) + version: 7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3) pure-rand: - specifier: ^7.0.1 - version: 7.0.1 + specifier: ^8.4.0 + version: 8.4.0 react: specifier: ^19.2.5 version: 19.2.5 @@ -153,23 +153,23 @@ importers: specifier: ^19.2.5 version: 19.2.5(react@19.2.5) react-error-boundary: - specifier: ^4.0.4 - version: 4.1.2(react@19.2.5) + specifier: ^6.1.1 + version: 6.1.1(react@19.2.5) react-resizable-panels: specifier: ^4.10.0 version: 4.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react-simple-maps: - specifier: ^2.3.0 - version: 2.3.0(prop-types@15.8.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: ^3.0.0 + version: 3.0.0(prop-types@15.8.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react-use-measure: specifier: ^2.0.4 version: 2.1.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react-window: - specifier: ^1.8.6 - version: 1.8.11(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: ^2.2.7 + version: 2.2.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5) redis: - specifier: ^4.5.1 - version: 4.7.1 + specifier: ^5.12.1 + version: 5.12.1 request-ip: specifier: ^3.3.0 version: 3.3.0 @@ -183,8 +183,8 @@ importers: specifier: ^7.7.4 version: 7.7.4 serialize-error: - specifier: ^12.0.0 - version: 12.0.0 + specifier: ^13.0.1 + version: 13.0.1 thenby: specifier: ^1.4.0 version: 1.4.0 @@ -208,32 +208,32 @@ importers: specifier: ^5.15.9 version: 5.15.9 '@rollup/plugin-alias': - specifier: ^5.0.0 - version: 5.1.1(rollup@4.60.1) + specifier: ^6.0.0 + version: 6.0.0(rollup@4.60.1) '@rollup/plugin-commonjs': - specifier: ^25.0.4 - version: 25.0.8(rollup@4.60.1) + specifier: ^29.0.2 + version: 29.0.2(rollup@4.60.1) '@rollup/plugin-json': specifier: ^6.0.0 version: 6.1.0(rollup@4.60.1) '@rollup/plugin-node-resolve': - specifier: ^15.2.0 - version: 15.3.1(rollup@4.60.1) + specifier: ^16.0.3 + version: 16.0.3(rollup@4.60.1) '@rollup/plugin-replace': - specifier: ^5.0.2 - version: 5.0.7(rollup@4.60.1) + specifier: ^6.0.3 + version: 6.0.3(rollup@4.60.1) '@rollup/plugin-terser': - specifier: ^0.4.4 - version: 0.4.4(rollup@4.60.1) + specifier: ^1.0.0 + version: 1.0.0(rollup@4.60.1) '@rollup/plugin-typescript': specifier: ^12.3.0 - version: 12.3.0(rollup@4.60.1)(tslib@2.8.1)(typescript@5.9.3) + version: 12.3.0(rollup@4.60.1)(tslib@2.8.1)(typescript@6.0.3) '@types/jest': specifier: ^30.0.0 version: 30.0.0 '@types/node': - specifier: ^24.12.2 - version: 24.12.2 + specifier: ^25.6.0 + version: 25.6.0 '@types/react': specifier: ^19.2.14 version: 19.2.14 @@ -247,14 +247,14 @@ importers: specifier: ^10.1.0 version: 10.1.0 cypress: - specifier: ^13.6.6 - version: 13.17.0 + specifier: ^15.14.0 + version: 15.14.0 dotenv-cli: specifier: ^11.0.0 version: 11.0.0 jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) + specifier: ^30.3.0 + version: 30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)) postcss: specifier: ^8.5.10 version: 8.5.10 @@ -262,11 +262,11 @@ importers: specifier: ^5.0.2 version: 5.0.2(postcss@8.5.10) postcss-import: - specifier: ^15.1.0 - version: 15.1.0(postcss@8.5.10) + specifier: ^16.1.1 + version: 16.1.1(postcss@8.5.10) postcss-preset-env: - specifier: 7.8.3 - version: 7.8.3(postcss@8.5.10) + specifier: 11.2.1 + version: 11.2.1(postcss@8.5.10) prompts: specifier: 2.4.2 version: 2.4.2 @@ -281,37 +281,37 @@ importers: version: 3.0.2(rollup@4.60.1) rollup-plugin-dts: specifier: ^6.4.1 - version: 6.4.1(rollup@4.60.1)(typescript@5.9.3) + version: 6.4.1(rollup@4.60.1)(typescript@6.0.3) rollup-plugin-node-externals: - specifier: ^8.1.1 - version: 8.1.2(rollup@4.60.1) + specifier: ^9.0.1 + version: 9.0.1(rollup@4.60.1) rollup-plugin-peer-deps-external: specifier: ^2.2.4 version: 2.2.4(rollup@4.60.1) rollup-plugin-postcss: specifier: ^4.0.2 - version: 4.0.2(postcss@8.5.10)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) + version: 4.0.2(postcss@8.5.10)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)) tar: specifier: ^7.5.13 version: 7.5.13 ts-jest: specifier: ^29.4.9 - version: 29.4.9(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.27.7)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.9(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.29.0))(esbuild@0.28.0)(jest-util@30.3.0)(jest@30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)))(typescript@6.0.3) ts-morph: - specifier: ^27.0.2 - version: 27.0.2 + specifier: ^28.0.0 + version: 28.0.0 ts-node: specifier: ^10.9.1 - version: 10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3) + version: 10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3) tsup: specifier: ^8.5.0 - version: 8.5.1(@swc/core@1.15.11(@swc/helpers@0.5.21))(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1) + version: 8.5.1(@swc/core@1.15.26(@swc/helpers@0.5.21))(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.1) tsx: specifier: ^4.19.0 version: 4.21.0 typescript: - specifier: ^5.9.3 - version: 5.9.3 + specifier: ^6.0.3 + version: 6.0.3 dist: dependencies: @@ -320,7 +320,7 @@ importers: version: 4.5.1 chartjs-adapter-date-fns: specifier: ^3.0.0 - version: 3.0.0(chart.js@4.5.1)(date-fns@2.30.0) + version: 3.0.0(chart.js@4.5.1)(date-fns@4.1.0) colord: specifier: ^2.9.2 version: 2.9.3 @@ -370,18 +370,34 @@ packages: resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + '@babel/core@7.28.3': resolution: {integrity: sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==} engines: {node: '>=6.9.0'} + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + '@babel/generator@7.28.3': resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} engines: {node: '>=6.9.0'} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.27.2': resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + '@babel/helper-globals@7.28.0': resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} @@ -390,14 +406,24 @@ packages: resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.28.3': resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-plugin-utils@7.27.1': - resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} engines: {node: '>=6.9.0'} '@babel/helper-string-parser@7.27.1': @@ -420,11 +446,20 @@ packages: resolution: {integrity: sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==} engines: {node: '>=6.9.0'} + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.28.3': resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-syntax-async-generators@7.8.4': resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: @@ -446,8 +481,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-attributes@7.27.1': - resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -462,8 +497,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-jsx@7.27.1': - resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -510,8 +545,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-typescript@7.27.1': - resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -520,18 +555,34 @@ packages: resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.28.3': resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + '@babel/types@7.28.2': resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -605,113 +656,331 @@ packages: resolution: {integrity: sha512-fuquQswRSHWM6D079ZeuGqkMOsqtcUPL06UdTnowmoeeYjVrqisfVmvnw8pc3OeKS4kVb91oygb/MfLDiMs0TQ==} engines: {node: '>=16'} - '@colors/colors@1.5.0': - resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} - engines: {node: '>=0.1.90'} - '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@csstools/postcss-cascade-layers@1.1.1': - resolution: {integrity: sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==} - engines: {node: ^12 || ^14 || >=16} + '@csstools/cascade-layer-name-parser@3.0.0': + resolution: {integrity: sha512-/3iksyevwRfSJx5yH0RkcrcYXwuhMQx3Juqf40t97PeEy2/Mz2TItZ/z/216qpe4GgOyFBP8MKIwVvytzHmfIQ==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 - '@csstools/postcss-color-function@1.1.1': - resolution: {integrity: sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==} - engines: {node: ^12 || ^14 || >=16} + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.0': + resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 - '@csstools/postcss-font-format-keywords@1.0.1': - resolution: {integrity: sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==} - engines: {node: ^12 || ^14 || >=16} + '@csstools/css-color-parser@4.1.0': + resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 - '@csstools/postcss-hwb-function@1.0.2': - resolution: {integrity: sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==} - engines: {node: ^12 || ^14 || >=16} + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + '@csstools/css-tokenizer': ^4.0.0 - '@csstools/postcss-ic-unit@1.0.1': - resolution: {integrity: sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==} - engines: {node: ^12 || ^14 || >=16} + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + + '@csstools/media-query-list-parser@5.0.0': + resolution: {integrity: sha512-T9lXmZOfnam3eMERPsszjY5NK0jX8RmThmmm99FZ8b7z8yMaFZWKwLWGZuTwdO3ddRY5fy13GmmEYZXB4I98Eg==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 - '@csstools/postcss-is-pseudo-class@2.0.7': - resolution: {integrity: sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==} - engines: {node: ^12 || ^14 || >=16} + '@csstools/postcss-alpha-function@2.0.4': + resolution: {integrity: sha512-fti7+GybzvfMrv5TSU6x8rWtXWOth5nLefT5w5AKJ3F3T0bZoxlRqajF0ZUgTtnytfMd4dQ8n5UiaNmsjFA65A==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 - '@csstools/postcss-nested-calc@1.0.0': - resolution: {integrity: sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==} - engines: {node: ^12 || ^14 || >=16} + '@csstools/postcss-cascade-layers@6.0.0': + resolution: {integrity: sha512-WhsECqmrEZQGqaPlBA7JkmF/CJ2/+wetL4fkL9sOPccKd32PQ1qToFM6gqSI5rkpmYqubvbxjEJhyMTHYK0vZQ==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 - '@csstools/postcss-normalize-display-values@1.0.1': - resolution: {integrity: sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==} - engines: {node: ^12 || ^14 || >=16} + '@csstools/postcss-color-function-display-p3-linear@2.0.3': + resolution: {integrity: sha512-u8QNV2TKOxG6cqK4ZrJkpctnxdrwdNTMrkyokmCi+iuLpJegOraA0cqC7HoxF2tHhxjuXc+BxwY/Qd62SwvanQ==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 - '@csstools/postcss-oklab-function@1.1.1': - resolution: {integrity: sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==} - engines: {node: ^12 || ^14 || >=16} + '@csstools/postcss-color-function@5.0.3': + resolution: {integrity: sha512-BiBukIeQ7rPjx9A//9+qgJugBjX6FY9eWiojbnfIJCPulWrl8J07rCgQbFkloTXena+a6Aw5xa25weU+3MA75A==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 - '@csstools/postcss-progressive-custom-properties@1.3.0': - resolution: {integrity: sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==} - engines: {node: ^12 || ^14 || >=16} + '@csstools/postcss-color-mix-function@4.0.3': + resolution: {integrity: sha512-M8ju3iqHRXtW1/5HYuOmi9WFR5rGGFgqkPh+kXkv/eG56oYK/WYtTeIwJgdcro7lRwjlo4Ut8xqbV3Iovkwfrw==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.3 + postcss: ^8.4 - '@csstools/postcss-stepped-value-functions@1.0.1': - resolution: {integrity: sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==} - engines: {node: ^12 || ^14 || >=16} + '@csstools/postcss-color-mix-variadic-function-arguments@2.0.3': + resolution: {integrity: sha512-tL46UyFjIjz7mDywoPOe/JgOpvMic0rsTUfdMBB1OHrUcCtE8MQpBILzYl/cAOtinJGu+ZQLuDhqTgTBOoeg3g==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 - '@csstools/postcss-text-decoration-shorthand@1.0.0': - resolution: {integrity: sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==} - engines: {node: ^12 || ^14 || >=16} + '@csstools/postcss-content-alt-text@3.0.0': + resolution: {integrity: sha512-OHa+4aCcrJtHpPWB3zptScHwpS1TUbeLR4uO0ntIz0Su/zw9SoWkVu+tDMSySSAsNtNSI3kut4fTliFwIsrHxA==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 - '@csstools/postcss-trigonometric-functions@1.0.2': - resolution: {integrity: sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==} - engines: {node: ^14 || >=16} + '@csstools/postcss-contrast-color-function@3.0.3': + resolution: {integrity: sha512-YcohXq+/hfYeobKirg3oXGivDaaTfOPv568bE3jYQCn9ILpFz+RgyJR/kF7ZWh5560TTlTjeCqF4ZmVsj2zwnw==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 - '@csstools/postcss-unset-value@1.0.2': - resolution: {integrity: sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==} - engines: {node: ^12 || ^14 || >=16} + '@csstools/postcss-exponential-functions@3.0.2': + resolution: {integrity: sha512-WDrfdFJXF4M67+wniEGr/5XVzsmn1rt2lL1YAlTfE7x7XDlRstTc5e+HuFoGv6jkiMWTwPsiADJaLwsnGC3UjQ==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 - '@csstools/selector-specificity@2.2.0': - resolution: {integrity: sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==} - engines: {node: ^14 || ^16 || >=18} + '@csstools/postcss-font-format-keywords@5.0.0': + resolution: {integrity: sha512-M1EjCe/J3u8fFhOZgRci74cQhJ7R0UFBX6T+WqoEvjrr8hVfMiV+HTYrzxLY5OW8YllvXYr5Q5t5OvJbsUSeDg==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss-selector-parser: ^6.0.10 + postcss: ^8.4 - '@cypress/request@3.0.9': - resolution: {integrity: sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw==} + '@csstools/postcss-font-width-property@1.0.0': + resolution: {integrity: sha512-AvmySApdijbjYQuXXh95tb7iVnqZBbJrv3oajO927ksE/mDmJBiszm+psW8orL2lRGR8j6ZU5Uv9/ou2Z5KRKA==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-gamut-mapping@3.0.3': + resolution: {integrity: sha512-3v5ZvcVuynhFh5qCJX2LIJ9Iry8/SvxfOEj6vDngNxbH/3OKTZBFLgK+DgLuIbsP1DLA9LLH3Rn7jmRxXgEDLA==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-gradients-interpolation-method@6.0.3': + resolution: {integrity: sha512-wrRIaRv1dkq30a8nvYWtSAf41bwCl+sVzLBKGnqeOwk81aSktKN3NattJpkiPyoOtEoFqChisl3WH3Csj/rOsw==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-hwb-function@5.0.3': + resolution: {integrity: sha512-bHz0uc/PBg2wJEAlGinUf494nMyuXsVKH/fExc2xGkvL6WHOKlxzx/lkn+2AVCQACtWBLVRCBDgDnkYr4RSC9w==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-ic-unit@5.0.0': + resolution: {integrity: sha512-/ws5d6c4uKqfM9zIL3ugcGI+3fvZEOOkJHNzAyTAGJIdZ+aSL9BVPNlHGV4QzmL0vqBSCOdU3+rhcMEj3+KzYw==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-initial@3.0.0': + resolution: {integrity: sha512-UVUrFmrTQyLomVepnjWlbBg7GoscLmXLwYFyjbcEnmpeGW7wde6lNpx5eM3eVwZI2M+7hCE3ykYnAsEPLcLa+Q==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-is-pseudo-class@6.0.0': + resolution: {integrity: sha512-1Hdy/ykg9RDo8vU8RiM2o+RaXO39WpFPaIkHxlAEJFofle/lc33tdQMKhBk3jR/Fe+uZNLOs3HlowFafyFptVw==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-light-dark-function@3.0.0': + resolution: {integrity: sha512-s++V5/hYazeRUCYIn2lsBVzUsxdeC46gtwpgW6lu5U/GlPOS5UTDT14kkEyPgXmFbCvaWLREqV7YTMJq1K3G6w==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-float-and-clear@4.0.0': + resolution: {integrity: sha512-NGzdIRVj/VxOa/TjVdkHeyiJoDihONV0+uB0csUdgWbFFr8xndtfqK8iIGP9IKJzco+w0hvBF2SSk2sDSTAnOQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-overflow@3.0.0': + resolution: {integrity: sha512-5cRg93QXVskM0MNepHpPcL0WLSf5Hncky0DrFDQY/4ozbH5lH7SX5ejayVpNTGSX7IpOvu7ykQDLOdMMGYzwpA==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-overscroll-behavior@3.0.0': + resolution: {integrity: sha512-82Jnl/5Wi5jb19nQE1XlBHrZcNL3PzOgcj268cDkfwf+xi10HBqufGo1Unwf5n8bbbEFhEKgyQW+vFsc9iY1jw==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-resize@4.0.0': + resolution: {integrity: sha512-L0T3q0gei/tGetCGZU0c7VN77VTivRpz1YZRNxjXYmW+85PKeI6U9YnSvDqLU2vBT2uN4kLEzfgZ0ThIZpN18A==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-viewport-units@4.0.0': + resolution: {integrity: sha512-TA3AqVN/1IH3dKRC2UUWvprvwyOs2IeD7FDZk5Hz20w4q33yIuSg0i0gjyTUkcn90g8A4n7QpyZ2AgBrnYPnnA==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-media-minmax@3.0.2': + resolution: {integrity: sha512-+ABxs2ZhJDhy+B9PJg7pgkGq6/d3XPXsWl7+6yZfAk4b2ba6aQ1h2AiTn04XwS6rpMpZEF3tONli/ubfu4y8AQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-media-queries-aspect-ratio-number-values@4.0.0': + resolution: {integrity: sha512-FDdC3lbrj8Vr0SkGIcSLTcRB7ApG6nlJFxOxkEF2C5hIZC1jtgjISFSGn/WjFdVkn8Dqe+Vx9QXI3axS2w1XHw==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-mixins@1.0.0': + resolution: {integrity: sha512-rz6qjT2w9L3k65jGc2dX+3oGiSrYQ70EZPDrINSmSVoVys7lLBFH0tvEa8DW2sr9cbRVD/W+1sy8+7bfu0JUfg==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-nested-calc@5.0.0': + resolution: {integrity: sha512-aPSw8P60e/i9BEfugauhikBqgjiwXcw3I9o4vXs+hktl4NSTgZRI0QHimxk9mst8N01A2TKDBxOln3mssRxiHQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-normalize-display-values@5.0.1': + resolution: {integrity: sha512-FcbEmoxDEGYvm2W3rQzVzcuo66+dDJjzzVDs+QwRmZLHYofGmMGwIKPqzF86/YW+euMDa7sh1xjWDvz/fzByZQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-oklab-function@5.0.3': + resolution: {integrity: sha512-vTMgJFMwMt9gnPvhKaDnMR7E/h9Nb+rPUv825SY5VUo4PWj+w0OH/N2NqgvjYeubaA3BVckbKDlvADATRpD4Hw==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-position-area-property@2.0.0': + resolution: {integrity: sha512-TeEfzsJGB23Syv7yCm8AHCD2XTFujdjr9YYu9ebH64vnfCEvY4BG319jXAYSlNlf3Yc9PNJ6WnkDkUF5XVgSKQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-progressive-custom-properties@5.0.0': + resolution: {integrity: sha512-NsJoZ89rxmDrUsITf8QIk5w+lQZQ8Xw5K6cLFG+cfiffsLYHb3zcbOOrHLetGl1WIhjWWQ4Cr8MMrg46Q+oACg==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-property-rule-prelude-list@2.0.0': + resolution: {integrity: sha512-qcMAkc9AhpzHgmQCD8hoJgGYifcOAxd1exXjjxilMM6euwRE619xDa4UsKBCv/v4g+sS63sd6c29LPM8s2ylSQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-random-function@3.0.2': + resolution: {integrity: sha512-iQ3vfX1LIqRXX7P1/ol45EpJ5CTWdQCAfdpTlHlsRPU4jMQeepmeNjQ0F60bj8RWTS1RkJ318fzzq4mUlyZ7hA==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-relative-color-syntax@4.0.3': + resolution: {integrity: sha512-SZSImz4KufmLi0dRwYivWXlza+7HF84SRApY8R48SyWgn+f0gDvmCn7D2Ie4CED7qU0JJK+YfCUC1HVlaQ10dg==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-scope-pseudo-class@5.0.0': + resolution: {integrity: sha512-kBrBFJcAji3MSHS4qQIihPvJfJC5xCabXLbejqDMiQi+86HD4eMBiTayAo46Urg7tlEmZZQFymFiJt+GH6nvXw==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-sign-functions@2.0.2': + resolution: {integrity: sha512-vOxkkMCMVnyaj7CW03uKR2R/zhJaCrptsXlm31HgI/dqC1lSIGnmu5W7N68x23XwcSgc8fE/fg0jKj4x1XFH4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-stepped-value-functions@5.0.2': + resolution: {integrity: sha512-4PtqkRoBcMSxZG00gcDv+nq7cxVUua+Yd7TmG16qzJjdolyICHkx1RfhNL5mKSnWOLxUnk/IdxAoWN+KU7E/ng==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-syntax-descriptor-syntax-production@2.0.0': + resolution: {integrity: sha512-elYcbdiBXAkPqvojB9kIBRuHY6htUhjSITtFQ+XiXnt6SvZCbNGxQmaaw6uZ7SPHu/+i/XVjzIt09/1k3SIerQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-system-ui-font-family@2.0.0': + resolution: {integrity: sha512-FyGZCgchFImFyiHS2x3rD5trAqatf/x23veBLTIgbaqyFfna6RNBD+Qf8HRSjt6HGMXOLhAjxJ3OoZg0bbn7Qw==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-text-decoration-shorthand@5.0.3': + resolution: {integrity: sha512-62fjggvIM1YYfDJPcErMUDkEZB6CByG8neTJqexnZe1hRBgCjD4dnXDLoCSSurjs1LzjBq6irFDpDaOvDZfrlw==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-trigonometric-functions@5.0.2': + resolution: {integrity: sha512-hRansZmQk1HH11WGUNlWy8H/DCB9Wy6zDbRcyBfF2UUP+V2fubK+qwmq0q6LIDje5gRzxlKyWhgFYxPy1ohivA==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-unset-value@5.0.0': + resolution: {integrity: sha512-EoO54sS2KCIfesvHyFYAW99RtzwHdgaJzhl7cqKZSaMYKZv3fXSOehDjAQx8WZBKn1JrMd7xJJI1T1BxPF7/jA==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@csstools/selector-resolve-nested@4.0.0': + resolution: {integrity: sha512-9vAPxmp+Dx3wQBIUwc1v7Mdisw1kbbaGqXUM8QLTgWg7SoPGYtXBsMXvsFs/0Bn5yoFhcktzxNZGNaUt0VjgjA==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss-selector-parser: ^7.1.1 + + '@csstools/selector-specificity@6.0.0': + resolution: {integrity: sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss-selector-parser: ^7.1.1 + + '@csstools/utilities@3.0.0': + resolution: {integrity: sha512-etDqA/4jYvOGBM6yfKCOsEXfH96BKztZdgGmGqKi2xHnDe0ILIBraRspwgYatJH9JsCZ5HCGoCst8w18EKOAdg==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + '@cypress/request@3.0.10': + resolution: {integrity: sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==} engines: {node: '>= 6'} '@cypress/xvfb@1.2.4': resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==} - '@date-fns/utc@1.2.0': - resolution: {integrity: sha512-YLq+crMPJiBmIdkRmv9nZuZy1mVtMlDcUKlg4mvI0UsC/dZeIaGoGB5p/C4FrpeOhZ7zBTK03T58S0DFkRNMnw==} + '@date-fns/utc@2.1.1': + resolution: {integrity: sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA==} '@dicebear/adventurer-neutral@9.4.2': resolution: {integrity: sha512-5xgkG/mNL4j3Q4SJGQLBU/KnU90tng8Ze5ofThD+55wi0oeY/nSAUowg6UFCmHrktjifj/MEx3CQqbpcPWtfIA==} @@ -923,9 +1192,18 @@ packages: '@electric-sql/pglite@0.4.1': resolution: {integrity: sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.9.2': resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@epic-web/invariant@1.0.0': resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} @@ -935,179 +1213,329 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.27.7': resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.27.7': resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.27.7': resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.27.7': resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.27.7': resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.27.7': resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.27.7': resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.27.7': resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.27.7': resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.27.7': resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.27.7': resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.27.7': resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.27.7': resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.27.7': resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.27.7': resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.27.7': resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.27.7': resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.27.7': resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.27.7': resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.27.7': resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.27.7': resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.27.7': resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.27.7': resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.27.7': resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.27.7': resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@formatjs/bigdecimal@0.2.0': - resolution: {integrity: sha512-GeaxHZbUoYvHL9tC5eltHLs+1zU70aPw0s7LwqgktIzF5oMhNY4o4deEtusJMsq7WFJF3Ye2zQEzdG8beVk73w==} + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] - '@formatjs/ecma402-abstract@3.2.0': - resolution: {integrity: sha512-dHnqHgBo6GXYGRsepaE1wmsC2etaivOWd5VaJstZd+HI2zR3DCUjbDVZRtoPGkkXZmyHvBwrdEUuqfvzhF/DtQ==} + '@formatjs/fast-memoize@3.1.2': + resolution: {integrity: sha512-vPnriihkfK0lzoQGaXq+qXH23VsYyansRTkTgo2aTG0k1NjLFyZimFVdfj4C9JkSE5dm7CEngcQ5TTc1yAyBfQ==} - '@formatjs/fast-memoize@3.1.1': - resolution: {integrity: sha512-CbNbf+tlJn1baRnPkNePnBqTLxGliG6DDgNa/UtV66abwIjwsliPMOt0172tzxABYzSuxZBZfcp//qI8AvBWPg==} + '@formatjs/icu-messageformat-parser@3.5.4': + resolution: {integrity: sha512-JVY39ROgLt+pIYngo6piyj4OVfZmXs/2FkC4wLS+ql1Eig/sGJKB7YwDO/5bkJFkfwaFAeIpgEiJc8hiYxNalw==} - '@formatjs/icu-messageformat-parser@3.5.3': - resolution: {integrity: sha512-HJWZ9S6JWey6iY5+YXE3Kd0ofWU1sC2KTTp56e1168g/xxWvVvr8k9G4fexIgwYV9wbtjY7kGYK5FjoWB3B2OQ==} + '@formatjs/icu-skeleton-parser@2.1.4': + resolution: {integrity: sha512-8bSFZbrlvGX11ywMZxtgkPBt5Q8/etyts7j7j+GWpOVK1g43zwMIH3LZxk43HAtEP7L/jtZ+OZaMiFTOiBj9CA==} - '@formatjs/icu-skeleton-parser@2.1.3': - resolution: {integrity: sha512-9mFp8TJ166ZM2pcjKwsBWXrDnOJGT7vMEScVgLygUODPOsE8S6f/FHoacvrlHK1B4dYZk8vSCNruyPU64AfgJQ==} + '@formatjs/intl-localematcher@0.8.3': + resolution: {integrity: sha512-pHUjWb9NuhnMs8+PxQdzBtZRFJHlGhrURGAbm6Ltwl82BFajeuiIR3jblSa7ia3r62rXe/0YtVpUG3xWr5bFCA==} - '@formatjs/intl-localematcher@0.8.2': - resolution: {integrity: sha512-q05KMYGJLyqFNFtIb8NhWLF5X3aK/k0wYt7dnRFuy6aLQL+vUwQ1cg5cO4qawEiINybeCPXAWlprY2mSBjSXAQ==} - - '@hello-pangea/dnd@17.0.0': - resolution: {integrity: sha512-LDDPOix/5N0j5QZxubiW9T0M0+1PR0rTDWeZF5pu1Tz91UQnuVK4qQ/EjY83Qm2QeX0eM8qDXANfDh3VVqtR4Q==} + '@hello-pangea/dnd@18.0.1': + resolution: {integrity: sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==} peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 '@hono/node-server@1.19.11': resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} @@ -1280,14 +1708,6 @@ packages: '@internationalized/string@3.2.8': resolution: {integrity: sha512-NdbMQUSfXLYIQol5VyMtinm9pZDciiMfN7RtmSuSB78io1hqwJ0naYfxyW6vgxWBkzWymQa/3uLDlbfmshtCaA==} - '@isaacs/balanced-match@4.0.1': - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} - engines: {node: 20 || >=22} - - '@isaacs/brace-expansion@5.0.0': - resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} - engines: {node: 20 || >=22} - '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1300,17 +1720,17 @@ packages: resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} - '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} engines: {node: '>=8'} - '@jest/console@29.7.0': - resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/console@30.3.0': + resolution: {integrity: sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/core@29.7.0': - resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/core@30.3.0': + resolution: {integrity: sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: @@ -1321,79 +1741,83 @@ packages: resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/environment@29.7.0': - resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/diff-sequences@30.3.0': + resolution: {integrity: sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/expect-utils@29.7.0': - resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/environment@30.3.0': + resolution: {integrity: sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/expect-utils@30.1.1': resolution: {integrity: sha512-5YUHr27fpJ64dnvtu+tt11ewATynrHkGYD+uSFgRr8V2eFJis/vEXgToyLwccIwqBihVfz9jwio+Zr1ab1Zihw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/expect@29.7.0': - resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/expect-utils@30.3.0': + resolution: {integrity: sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/fake-timers@29.7.0': - resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/expect@30.3.0': + resolution: {integrity: sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/fake-timers@30.3.0': + resolution: {integrity: sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/get-type@30.1.0': resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/globals@29.7.0': - resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/globals@30.3.0': + resolution: {integrity: sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/pattern@30.0.1': resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/reporters@29.7.0': - resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/reporters@30.3.0': + resolution: {integrity: sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: node-notifier: optional: true - '@jest/schemas@29.6.3': - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/schemas@30.0.5': resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/source-map@29.6.3': - resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/snapshot-utils@30.3.0': + resolution: {integrity: sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/test-result@29.7.0': - resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/source-map@30.0.1': + resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/test-sequencer@29.7.0': - resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/test-result@30.3.0': + resolution: {integrity: sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/transform@29.7.0': - resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/test-sequencer@30.3.0': + resolution: {integrity: sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/types@29.6.3': - resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/transform@30.3.0': + resolution: {integrity: sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/types@30.0.5': resolution: {integrity: sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/types@30.3.0': + resolution: {integrity: sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -1422,6 +1846,9 @@ packages: '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@netlify/plugin-nextjs@5.15.9': resolution: {integrity: sha512-4kJvV+wrt4/ncehfaOXfhv1FYKCv9EFjq6/tgXXYSlqPDcbsuTXR0DTz9IvyZ/CpPRadRD/zzIqD9wXgCmA2lg==} engines: {node: '>=18.0.0'} @@ -1585,6 +2012,10 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@prisma/adapter-pg@7.7.0': resolution: {integrity: sha512-q33Ta8sKbgzEpAy0lx45tAq//yMv0qcb+8nj+TCA3P4wiAY+OBFEFk/NDkZncAfHaNJeGo5WJpJdpbL+ijYx8g==} @@ -1823,47 +2254,54 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@redis/bloom@1.2.0': - resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} + '@redis/bloom@5.12.1': + resolution: {integrity: sha512-PUUfv+ms7jgPSBVoo/DN4AkPHj4D5TZSd6SbJX7egzBplkYUcKmHRE8RKia7UtZ8bSQbLguLvxVO+asKtQfZWA==} + engines: {node: '>= 18.19.0'} peerDependencies: - '@redis/client': ^1.0.0 + '@redis/client': ^5.12.1 - '@redis/client@1.6.1': - resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==} - engines: {node: '>=14'} - - '@redis/graph@1.1.1': - resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} + '@redis/client@5.12.1': + resolution: {integrity: sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==} + engines: {node: '>= 18.19.0'} peerDependencies: - '@redis/client': ^1.0.0 + '@node-rs/xxhash': ^1.1.0 + '@opentelemetry/api': '>=1 <2' + peerDependenciesMeta: + '@node-rs/xxhash': + optional: true + '@opentelemetry/api': + optional: true - '@redis/json@1.0.7': - resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==} + '@redis/json@5.12.1': + resolution: {integrity: sha512-eOze75esLve4vfqDel7aMX08CNaiLLQS2fV8mpRN9NxPe1rVR4vQyYiW/OgtGUysF6QOr9ANhfxABKNOJfXdKg==} + engines: {node: '>= 18.19.0'} peerDependencies: - '@redis/client': ^1.0.0 + '@redis/client': ^5.12.1 - '@redis/search@1.2.0': - resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==} + '@redis/search@5.12.1': + resolution: {integrity: sha512-ItlxbxC9cKI6IU1TLWoczwJCRb6TdmkEpWv05UrPawqaAnWGRu3rcIqsc5vN483T2fSociuyV1UkWIL5I4//2w==} + engines: {node: '>= 18.19.0'} peerDependencies: - '@redis/client': ^1.0.0 + '@redis/client': ^5.12.1 - '@redis/time-series@1.1.0': - resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==} + '@redis/time-series@5.12.1': + resolution: {integrity: sha512-c6JL6E3EcZJuNqKFz+KM+l9l5mpcQiKvTwgA3blt5glWJ8hjDk0yeHN3beE/MpqYIQ8UEX44ItQzgkE/gCBELQ==} + engines: {node: '>= 18.19.0'} peerDependencies: - '@redis/client': ^1.0.0 + '@redis/client': ^5.12.1 - '@rollup/plugin-alias@5.1.1': - resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==} - engines: {node: '>=14.0.0'} + '@rollup/plugin-alias@6.0.0': + resolution: {integrity: sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g==} + engines: {node: '>=20.19.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + rollup: '>=4.0.0' peerDependenciesMeta: rollup: optional: true - '@rollup/plugin-commonjs@25.0.8': - resolution: {integrity: sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A==} - engines: {node: '>=14.0.0'} + '@rollup/plugin-commonjs@29.0.2': + resolution: {integrity: sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} peerDependencies: rollup: ^2.68.0||^3.0.0||^4.0.0 peerDependenciesMeta: @@ -1879,8 +2317,8 @@ packages: rollup: optional: true - '@rollup/plugin-node-resolve@15.3.1': - resolution: {integrity: sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==} + '@rollup/plugin-node-resolve@16.0.3': + resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} engines: {node: '>=14.0.0'} peerDependencies: rollup: ^2.78.0||^3.0.0||^4.0.0 @@ -1888,8 +2326,8 @@ packages: rollup: optional: true - '@rollup/plugin-replace@5.0.7': - resolution: {integrity: sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ==} + '@rollup/plugin-replace@6.0.3': + resolution: {integrity: sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==} engines: {node: '>=14.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 @@ -1897,9 +2335,9 @@ packages: rollup: optional: true - '@rollup/plugin-terser@0.4.4': - resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} - engines: {node: '>=14.0.0'} + '@rollup/plugin-terser@1.0.0': + resolution: {integrity: sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==} + engines: {node: '>=20.0.0'} peerDependencies: rollup: ^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: @@ -1928,6 +2366,15 @@ packages: rollup: optional: true + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.60.1': resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} cpu: [arm] @@ -2072,9 +2519,6 @@ packages: '@schummar/icu-type-parser@1.21.5': resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==} - '@sinclair/typebox@0.27.8': - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - '@sinclair/typebox@0.34.40': resolution: {integrity: sha512-gwBNIP8ZAYev/ORDWW0QvxdwPXwxBtLsdsJgSc7eDIRt8ubP+rxUBzPsrwnu16fgEF8Bx4lh/+mvQvJzcTM6Kw==} @@ -2085,8 +2529,8 @@ packages: '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} - '@sinonjs/fake-timers@10.3.0': - resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@sinonjs/fake-timers@15.3.2': + resolution: {integrity: sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==} '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -2176,72 +2620,86 @@ packages: peerDependencies: '@svgr/core': '*' - '@swc/core-darwin-arm64@1.15.11': - resolution: {integrity: sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg==} + '@swc/core-darwin-arm64@1.15.26': + resolution: {integrity: sha512-OmcP96CFsNOwa65tamQayRcfqhNlcQ3YCWOq+0Wb+CAM4uB7kOMrXY41Gj4atthxrGhLQ9pg7Vk26iApb88idA==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.15.11': - resolution: {integrity: sha512-S52Gu1QtPSfBYDiejlcfp9GlN+NjTZBRRNsz8PNwBgSE626/FUf2PcllVUix7jqkoMC+t0rS8t+2/aSWlMuQtA==} + '@swc/core-darwin-x64@1.15.26': + resolution: {integrity: sha512-liTTTpKSv89ivIxcZ+iU1cRige9Y7JkOjVnJ2Ystzl+DsWNHqt7wLTTgm/u7gEqmmAS2JKryODLQn3q1UtFNPQ==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.15.11': - resolution: {integrity: sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg==} + '@swc/core-linux-arm-gnueabihf@1.15.26': + resolution: {integrity: sha512-Y/g+m3I8CeBof5A3kWWOS6QA2HOIUytF5EeTgfwcAK+GKT/tGe7Xqo5svBtaqflU5od2zzbMTWqkinPXgRWGgA==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.15.11': - resolution: {integrity: sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA==} + '@swc/core-linux-arm64-gnu@1.15.26': + resolution: {integrity: sha512-19IvwyPfBN/rz9s7qXhOTQmW0922+pjpRUUvIebu+CMM75nX6YuDzHsGx8hSmn5dS89SNaMCh1lgUuXqm++6jg==} engines: {node: '>=10'} cpu: [arm64] os: [linux] libc: [glibc] - '@swc/core-linux-arm64-musl@1.15.11': - resolution: {integrity: sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==} + '@swc/core-linux-arm64-musl@1.15.26': + resolution: {integrity: sha512-iNlbvTIo425rkKzDLLWFJGnFXr3myETUdIDHcjuiPNZE8b0ogmcAuilC4yEJX7FSHGbnlsoJcCT2xf4b3VJmmQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] libc: [musl] - '@swc/core-linux-x64-gnu@1.15.11': - resolution: {integrity: sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==} + '@swc/core-linux-ppc64-gnu@1.15.26': + resolution: {integrity: sha512-AuuEOtG+YXKIjIUup4RsxYNklx6XVB3WKWfhxG6hnfPrn7vp89RNOLbbyyprgj6Sk7k9ulwGVTJElEvmBNPSCA==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-s390x-gnu@1.15.26': + resolution: {integrity: sha512-JcMDWQvW1BchUyRg8E0jHiTx7CQYpUr5uDEL1dnPDECrEjBEGG2ynmJ3XX70sWXql0JagqR1t3VpANYFWdUnqA==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-gnu@1.15.26': + resolution: {integrity: sha512-FW7V7Mbpq4+PA7BiAq76LJs8MdNuUSylyuRVfQRkhIyeWadFroZ+KOPgjku8Z/fXzngxBRvsk+PGGB0t8mGcjA==} engines: {node: '>=10'} cpu: [x64] os: [linux] libc: [glibc] - '@swc/core-linux-x64-musl@1.15.11': - resolution: {integrity: sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==} + '@swc/core-linux-x64-musl@1.15.26': + resolution: {integrity: sha512-w8erqMHsVcdGwUfJxF6LaiTuPoKnyLOcUbhLcxiXrlLt5MLjtlgcIeUY/NWK/oPoyqkgH+/i8pOJnMTxvl83ZQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] libc: [musl] - '@swc/core-win32-arm64-msvc@1.15.11': - resolution: {integrity: sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==} + '@swc/core-win32-arm64-msvc@1.15.26': + resolution: {integrity: sha512-uDCWCNpUiqkbvPmsuPUTn/P7ag9SqNXD2JT/W3dUu7yZ2krzN+nmmoQ2xRX63/J6RYiHI7aT4jo7Z++lsljlPA==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.15.11': - resolution: {integrity: sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw==} + '@swc/core-win32-ia32-msvc@1.15.26': + resolution: {integrity: sha512-2k1ax1QmmqLEnpC0uRCw7OXhBfyvdPqERBXupDasjYbChT6ZSO/uha28Bp38cw0viKIG79L27aTDkbkABsMW3w==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.15.11': - resolution: {integrity: sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw==} + '@swc/core-win32-x64-msvc@1.15.26': + resolution: {integrity: sha512-aUuYecSEGa4SUSdyCWaI/vk8jdseifYnsF1GZQx2+piL8GIuT/5QrVcFfmes4Iwy7FIVXxtzD063z/FfpZ7K7w==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.15.11': - resolution: {integrity: sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w==} + '@swc/core@1.15.26': + resolution: {integrity: sha512-tglZGyx8N5PC+x1Nd/JrZxqpqlcZoSuG9gTDKO6AuFToFiVB3uS8HvbKFuO7g3lJzvFf9riAb94xs9HU2UhAHQ==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -2261,8 +2719,8 @@ packages: '@swc/helpers@0.5.21': resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} - '@swc/types@0.1.25': - resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} + '@swc/types@0.1.26': + resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} '@tanstack/query-core@5.99.0': resolution: {integrity: sha512-3Jv3WQG0BCcH7G+7lf/bP8QyBfJOXeY+T08Rin3GZ1bshvwlbPt7NrDHMEzGdKIOmOzvIQmxjk28YEQX60k7pQ==} @@ -2276,8 +2734,8 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} - '@ts-morph/common@0.28.1': - resolution: {integrity: sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==} + '@ts-morph/common@0.29.0': + resolution: {integrity: sha512-35oUmphHbJvQ/+UTwFNme/t2p3FoKiGJ5auTjjpNTop2dyREspirjMy82PLSC1pnDJ8ah1GU98hwpVt64YXQsg==} '@tsconfig/node10@1.0.11': resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} @@ -2294,6 +2752,9 @@ packages: '@tsconfig/svelte@1.0.13': resolution: {integrity: sha512-5lYJP45Xllo4yE/RUBccBT32eBlRDbqN8r1/MIvQbKxW3aFqaYPCNgm8D5V20X4ShHcwvYWNlKg3liDh1MlBoA==} + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -2318,9 +2779,6 @@ packages: '@types/glob@7.2.0': resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} - '@types/graceful-fs@4.1.9': - resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} - '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -2340,8 +2798,8 @@ packages: resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==} deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed. - '@types/node@24.12.2': - resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} @@ -2360,12 +2818,15 @@ packages: '@types/sinonjs__fake-timers@8.1.1': resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==} - '@types/sizzle@2.3.9': - resolution: {integrity: sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==} + '@types/sizzle@2.3.10': + resolution: {integrity: sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/tmp@0.2.6': + resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==} + '@types/use-sync-external-store@0.0.6': resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} @@ -2375,6 +2836,9 @@ packages: '@types/yargs@17.0.33': resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -2385,6 +2849,112 @@ packages: react-aria-components: ^1.0.0 react-dom: ^18.0.0 || ^19.0.0 + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + '@xstate/fsm@1.6.5': resolution: {integrity: sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==} @@ -2397,6 +2967,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} @@ -2486,9 +3061,6 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} - async@3.2.6: - resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -2496,8 +3068,8 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} - autoprefixer@10.4.21: - resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -2517,19 +3089,19 @@ packages: aws4@1.13.2: resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} - babel-jest@29.7.0: - resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + babel-jest@30.3.0: + resolution: {integrity: sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: - '@babel/core': ^7.8.0 + '@babel/core': ^7.11.0 || ^8.0.0-0 - babel-plugin-istanbul@6.1.1: - resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} - engines: {node: '>=8'} + babel-plugin-istanbul@7.0.1: + resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==} + engines: {node: '>=12'} - babel-plugin-jest-hoist@29.6.3: - resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + babel-plugin-jest-hoist@30.3.0: + resolution: {integrity: sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} babel-plugin-react-compiler@19.1.0-rc.2: resolution: {integrity: sha512-kSNA//p5fMO6ypG8EkEVPIqAjwIXm5tMjfD1XRPL/sRjYSbJ6UsvORfaeolNWnZ9n310aM0xJP7peW26BuCVzA==} @@ -2539,15 +3111,19 @@ packages: peerDependencies: '@babel/core': ^7.0.0 || ^8.0.0-0 - babel-preset-jest@29.6.3: - resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + babel-preset-jest@30.3.0: + resolution: {integrity: sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': ^7.11.0 || ^8.0.0-beta.1 balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + base64-arraybuffer@1.0.2: resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} engines: {node: '>= 0.6.0'} @@ -2583,9 +3159,16 @@ packages: brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -2595,6 +3178,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + bs-logger@0.2.6: resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} engines: {node: '>= 6'} @@ -2698,10 +3286,6 @@ packages: chart.js: '>=2.8.0' date-fns: '>=2.0.0' - check-more-types@2.24.0: - resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} - engines: {node: '>= 0.8.0'} - chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -2710,22 +3294,22 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} - ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} - ci-info@4.3.0: resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==} engines: {node: '>=8'} + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} citty@0.2.2: resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} - cjs-module-lexer@1.4.3: - resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} @@ -2738,8 +3322,8 @@ packages: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} - cli-table3@0.6.5: - resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + cli-table3@0.6.1: + resolution: {integrity: sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==} engines: {node: 10.* || >= 12.*} cli-truncate@2.1.0: @@ -2768,8 +3352,8 @@ packages: code-block-writer@13.0.3: resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} - collect-v8-coverage@1.0.2: - resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + collect-v8-coverage@1.0.3: + resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -2793,6 +3377,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + colors@1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -2861,11 +3449,6 @@ packages: typescript: optional: true - create-jest@29.7.0: - resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -2882,10 +3465,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - css-blank-pseudo@3.0.3: - resolution: {integrity: sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==} - engines: {node: ^12 || ^14 || >=16} - hasBin: true + css-blank-pseudo@8.0.1: + resolution: {integrity: sha512-C5B2e5hCM4llrQkUms+KnWEMVW8K1n2XvX9G7ppfMZJQ7KAS/4rNnkP1Cs+HhWriOz1mWWTMFD4j1J7s31Dgug==} + engines: {node: '>=20.19.0'} peerDependencies: postcss: ^8.4 @@ -2898,17 +3480,15 @@ packages: peerDependencies: postcss: ^8.0.9 - css-has-pseudo@3.0.4: - resolution: {integrity: sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==} - engines: {node: ^12 || ^14 || >=16} - hasBin: true + css-has-pseudo@8.0.0: + resolution: {integrity: sha512-Uz/bsHRbOeir/5Oeuz85tq/yLJLxX+3dpoRdjNTshs6jjqwUg8XaEZGDd0ci3fw7l53Srw0EkJ8mYan0eW5uGQ==} + engines: {node: '>=20.19.0'} peerDependencies: postcss: ^8.4 - css-prefers-color-scheme@6.0.3: - resolution: {integrity: sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==} - engines: {node: ^12 || ^14 || >=16} - hasBin: true + css-prefers-color-scheme@11.0.0: + resolution: {integrity: sha512-fv0mgtwUhh2m9iio3Kxc2CkrogjIaRdMFaaqyzSFdii17JF4cfPyMNX72B15ZW2Nrr/NZUpxI4dec1VMHYJvdw==} + engines: {node: '>=20.19.0'} peerDependencies: postcss: ^8.4 @@ -2934,8 +3514,8 @@ packages: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} - cssdb@7.11.2: - resolution: {integrity: sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A==} + cssdb@8.8.0: + resolution: {integrity: sha512-QbLeyz2Bgso1iRlh7IpWk6OKa3lLNGXsujVjDMPl9rOZpxKeiG69icLpbLCFxeURwmcdIfZqQyhlooKJYM4f8Q==} cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} @@ -2971,9 +3551,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - cypress@13.17.0: - resolution: {integrity: sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==} - engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} + cypress@15.14.0: + resolution: {integrity: sha512-AHt9YLKVl6uOFfXsLM3+LSZFwsx36BJRyFv4CjsqcRgr+V9kir0IPVRZUgqZVNudRalx771A1c4yR3DmXvSiBQ==} + engines: {node: ^20.1.0 || ^22.0.0 || >=24.0.0} hasBin: true d3-array@2.12.1: @@ -3035,17 +3615,16 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} - date-fns-tz@1.3.8: - resolution: {integrity: sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==} + date-fns-tz@3.2.0: + resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==} peerDependencies: - date-fns: '>=2.0.0' + date-fns: ^3.0.0 || ^4.0.0 - date-fns@2.30.0: - resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} - engines: {node: '>=0.11'} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} - dayjs@1.11.13: - resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} @@ -3064,8 +3643,8 @@ packages: supports-color: optional: true - dedent@1.6.0: - resolution: {integrity: sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==} + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} peerDependencies: babel-plugin-macros: ^3.1.0 peerDependenciesMeta: @@ -3091,10 +3670,6 @@ packages: defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} - del@6.1.1: - resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} - engines: {node: '>=10'} - del@8.0.1: resolution: {integrity: sha512-gPqh0mKTPvaUZGAuHbrBUYKZWBNAeHG7TU3QH5EhVwPMyKvmfJaNXhcD2jTcXsJRRcffuho4vaYweu80dRrMGA==} engines: {node: '>=18'} @@ -3124,10 +3699,6 @@ packages: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} - diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -3197,6 +3768,9 @@ packages: electron-to-chromium@1.5.202: resolution: {integrity: sha512-NxbYjRmiHcHXV1Ws3fWUW+SLb62isauajk45LUJ/HgIOkUA7jLZu/X2Iif+X9FBNK8QkF9Zb4Q2mcwXCcY30mg==} + electron-to-chromium@1.5.340: + resolution: {integrity: sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==} + emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} @@ -3261,6 +3835,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -3302,18 +3881,18 @@ packages: resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} engines: {node: '>=4'} - exit@0.1.2: - resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + exit-x@0.2.2: + resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} engines: {node: '>= 0.8.0'} - expect@29.7.0: - resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - expect@30.1.1: resolution: {integrity: sha512-OKe7cdic4qbfWd/CcgwJvvCrNX2KWfuMZee9AfJHL1gTYmvqjBjZG1a2NwfhspBzxzlXwsN75WWpKTYfsJpBxg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + expect@30.3.0: + resolution: {integrity: sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -3397,16 +3976,16 @@ packages: forever-agent@0.6.1: resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} - form-data@4.0.4: - resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} - fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} fs-extra@8.1.0: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} @@ -3440,10 +4019,6 @@ packages: generic-names@4.0.0: resolution: {integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==} - generic-pool@3.9.0: - resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} - engines: {node: '>= 4'} - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -3482,9 +4057,6 @@ packages: get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} - getos@3.2.1: - resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==} - getpass@0.1.7: resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} @@ -3522,10 +4094,6 @@ packages: resolution: {integrity: sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==} engines: {node: '>=8'} - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - globby@14.1.0: resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} engines: {node: '>=18'} @@ -3575,10 +4143,18 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + hasha@5.2.2: + resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==} + engines: {node: '>=8'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + hono@4.12.9: resolution: {integrity: sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==} engines: {node: '>=16.9.0'} @@ -3617,8 +4193,8 @@ packages: peerDependencies: postcss: ^8.1.0 - icu-minify@4.8.4: - resolution: {integrity: sha512-lRfPX67iofadgfhMvjs37GqLB8ErDaNERte2XNpm69bP10D6RFV/tAWYX0lEBwsdLWxpliKVh2TT4NEa63yuWw==} + icu-minify@4.9.1: + resolution: {integrity: sha512-6NkfF9GHHFouqnz+wuiLjCWQiyxoEyJ5liUv4Jxxo/8wyhV7MY0L0iTEGDAVEa4aAD58WqTxFMa20S5nyMjwNw==} ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -3680,8 +4256,8 @@ packages: internmap@1.0.1: resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} - intl-messageformat@11.2.0: - resolution: {integrity: sha512-IhghAA8n4KSlXuWKzYsWyWb82JoYTzShfyvdSF85oJPnNOjvv4kAo7S7Jtkm3/vJ53C7dQNRO+Gpnj3iWgTjBQ==} + intl-messageformat@11.2.1: + resolution: {integrity: sha512-1gAVEUt3wEPvTqML4Fsw9klZV5j0vszQxayP/fi6gUroAc8AUHiNaisBKLWxybL1AdWq1mP07YV1q8v4N92ilQ==} ipaddr.js@2.3.0: resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} @@ -3710,8 +4286,8 @@ packages: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - is-ci@3.0.1: - resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + is-ci@4.1.0: + resolution: {integrity: sha512-Ab9bQDQ11lWootZUI5qxgN2ZXwxNI5hTwnsvOc1wyxQ7zQ8OkEDw79mI0+9jI3x432NfwbVRru+3noJfXF6lSQ==} hasBin: true is-core-module@2.16.1: @@ -3726,9 +4302,9 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} - is-docker@3.0.0: - resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-docker@4.0.0: + resolution: {integrity: sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==} + engines: {node: '>=20'} hasBin: true is-extglob@2.1.1: @@ -3759,9 +4335,9 @@ packages: resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} engines: {node: '>=10'} - is-localhost-ip@2.0.0: - resolution: {integrity: sha512-vlgs2cSgMOfnKU8c1ewgKPyum9rVrjjLLW2HBdL5i0iAJjOs8NY55ZBd/hqUTaYR0EO9CKZd3hVSC2HlIbygTQ==} - engines: {node: '>=12'} + is-localhost-ip@3.0.1: + resolution: {integrity: sha512-OwNA8bw72L/gDVfCrhJ3GFb3AGp+I7t+cvJaQGkiLZgeBAZmAygIduAdhLq56JycuJ68NCICxA2yyIaAtbR9Kg==} + engines: {node: '>=18'} is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} @@ -3782,10 +4358,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-cwd@2.2.0: - resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} - engines: {node: '>=6'} - is-path-cwd@3.0.0: resolution: {integrity: sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3878,10 +4450,6 @@ packages: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} - istanbul-lib-instrument@5.2.1: - resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} - engines: {node: '>=8'} - istanbul-lib-instrument@6.0.3: resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} engines: {node: '>=10'} @@ -3890,28 +4458,28 @@ packages: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} - istanbul-lib-source-maps@4.0.1: - resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} engines: {node: '>=10'} - istanbul-reports@3.1.7: - resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jest-changed-files@29.7.0: - resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-changed-files@30.3.0: + resolution: {integrity: sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-circus@29.7.0: - resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-circus@30.3.0: + resolution: {integrity: sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-cli@29.7.0: - resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-cli@30.3.0: + resolution: {integrity: sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -3919,74 +4487,73 @@ packages: node-notifier: optional: true - jest-config@29.7.0: - resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-config@30.3.0: + resolution: {integrity: sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@types/node': '*' + esbuild-register: '>=3.4.0' ts-node: '>=9.0.0' peerDependenciesMeta: '@types/node': optional: true + esbuild-register: + optional: true ts-node: optional: true - jest-diff@29.7.0: - resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-diff@30.1.1: resolution: {integrity: sha512-LUU2Gx8EhYxpdzTR6BmjL1ifgOAQJQELTHOiPv9KITaKjZvJ9Jmgigx01tuZ49id37LorpGc9dPBPlXTboXScw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-docblock@29.7.0: - resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-diff@30.3.0: + resolution: {integrity: sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-each@29.7.0: - resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-docblock@30.2.0: + resolution: {integrity: sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-environment-node@29.7.0: - resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-each@30.3.0: + resolution: {integrity: sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-get-type@29.6.3: - resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-environment-node@30.3.0: + resolution: {integrity: sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-haste-map@29.7.0: - resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-haste-map@30.3.0: + resolution: {integrity: sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-leak-detector@29.7.0: - resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-matcher-utils@29.7.0: - resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-leak-detector@30.3.0: + resolution: {integrity: sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-matcher-utils@30.1.1: resolution: {integrity: sha512-SuH2QVemK48BNTqReti6FtjsMPFsSOD/ZzRxU1TttR7RiRsRSe78d03bb4Cx6D4bQC/80Q8U4VnaaAH9FlbZ9w==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-message-util@29.7.0: - resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-matcher-utils@30.3.0: + resolution: {integrity: sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-message-util@30.1.0: resolution: {integrity: sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-mock@29.7.0: - resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-message-util@30.3.0: + resolution: {integrity: sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-mock@30.0.5: resolution: {integrity: sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-mock@30.3.0: + resolution: {integrity: sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-pnp-resolver@1.2.3: resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} engines: {node: '>=6'} @@ -3996,57 +4563,53 @@ packages: jest-resolve: optional: true - jest-regex-util@29.6.3: - resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-regex-util@30.0.1: resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-resolve-dependencies@29.7.0: - resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-resolve-dependencies@30.3.0: + resolution: {integrity: sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-resolve@29.7.0: - resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-resolve@30.3.0: + resolution: {integrity: sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-runner@29.7.0: - resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-runner@30.3.0: + resolution: {integrity: sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-runtime@29.7.0: - resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-runtime@30.3.0: + resolution: {integrity: sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-snapshot@29.7.0: - resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-util@29.7.0: - resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-snapshot@30.3.0: + resolution: {integrity: sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-util@30.0.5: resolution: {integrity: sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-validate@29.7.0: - resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-util@30.3.0: + resolution: {integrity: sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-watcher@29.7.0: - resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-validate@30.3.0: + resolution: {integrity: sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-worker@29.7.0: - resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-watcher@30.3.0: + resolution: {integrity: sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest@29.7.0: - resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-worker@30.3.0: + resolution: {integrity: sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest@30.3.0: + resolution: {integrity: sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -4065,8 +4628,8 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true js-yaml@4.1.0: @@ -4132,10 +4695,6 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - lazy-ass@1.6.0: - resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==} - engines: {node: '> 0.8'} - leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -4209,8 +4768,8 @@ packages: lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} @@ -4255,9 +4814,6 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -4291,9 +4847,6 @@ packages: memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} - memoize-one@6.0.0: - resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} - memorystream@0.3.1: resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} engines: {node: '>= 0.10.0'} @@ -4321,13 +4874,16 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - minimatch@10.0.3: - resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} - engines: {node: 20 || >=22} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} @@ -4376,6 +4932,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -4386,15 +4947,15 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - next-intl-swc-plugin-extractor@4.8.4: - resolution: {integrity: sha512-iA9/jaDfFTQYypnbLP7UGO0x3z2uuvHdNgIOtl53EwlVvQpp1MW77qKF+v87uYBoAwyniwGEhNXsO6qWEI+EzA==} + next-intl-swc-plugin-extractor@4.9.1: + resolution: {integrity: sha512-8whJJ6oxJz8JqkHarggmmuEDyXgC7nEnaPhZD91CJwEWW4xp0AST3Mw17YxvHyP2vAF3taWfFbs1maD+WWtz3w==} - next-intl@4.8.3: - resolution: {integrity: sha512-PvdBDWg+Leh7BR7GJUQbCDVVaBRn37GwDBWc9sv0rVQOJDQ5JU1rVzx9EEGuOGYo0DHAl70++9LQ7HxTawdL7w==} + next-intl@4.9.1: + resolution: {integrity: sha512-N7ga0CjtYcdxNvaKNIi6eJ2mmatlHK5hp8rt0YO2Omoc1m0gean242/Ukdj6+gJNiReBVcYIjK0HZeNx7CV1ug==} peerDependencies: next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 - typescript: ^5.0.0 + typescript: '*' peerDependenciesMeta: typescript: optional: true @@ -4447,6 +5008,13 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + + non-error@0.1.0: + resolution: {integrity: sha512-TMB1uHiGsHRGv1uYclfhivcnf0/PdFp2pNqRxXjncaAsjYMoisaQJI+SSZCqRq+VliwRTC8tsMQfmrWjDMhkPQ==} + engines: {node: '>=20'} + normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -4454,10 +5022,6 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - normalize-url@6.1.0: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} @@ -4656,14 +5220,18 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pidtree@0.3.1: resolution: {integrity: sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==} engines: {node: '>=0.10'} @@ -4702,11 +5270,11 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss-attribute-case-insensitive@5.0.2: - resolution: {integrity: sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==} - engines: {node: ^12 || ^14 || >=16} + postcss-attribute-case-insensitive@8.0.0: + resolution: {integrity: sha512-fovIPEV35c2JzVXdmP+sp2xirbBMt54J+upU8u6TSj410kUU5+axgEzvBBSAX8KCybze8CFCelzFAw/FfWg2TA==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 postcss-calc@8.2.4: resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} @@ -4719,23 +5287,23 @@ packages: peerDependencies: postcss: ^8.4.6 - postcss-color-functional-notation@4.2.4: - resolution: {integrity: sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.2 - - postcss-color-hex-alpha@8.0.4: - resolution: {integrity: sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==} - engines: {node: ^12 || ^14 || >=16} + postcss-color-functional-notation@8.0.3: + resolution: {integrity: sha512-MyaFK+3PusD7F2+qlMDP6+zfSgHWP17AtmvHQs44W3+Qbb39VptVDVRJ4Lf7gHSVffW5ekEy/XrsZ0S0t34hrA==} + engines: {node: '>=20.19.0'} peerDependencies: postcss: ^8.4 - postcss-color-rebeccapurple@7.1.1: - resolution: {integrity: sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==} - engines: {node: ^12 || ^14 || >=16} + postcss-color-hex-alpha@11.0.0: + resolution: {integrity: sha512-NCGa6vjIyrjosz9GqRxVKbONBklz5TeipYqTJp3IqbnBWlBq5e5EMtG6MaX4vqk9LzocPfMQkuRK9tfk+OQuKg==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 + + postcss-color-rebeccapurple@11.0.0: + resolution: {integrity: sha512-g9561mx7cbdqx7XeO/L+lJzVlzu7bICyXr72efBVKZGxIhvBBJf9fGXn3Cb6U4Bwh3LbzQO2e9NWBLVYdX5Eag==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 postcss-colormin@5.3.1: resolution: {integrity: sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==} @@ -4749,29 +5317,29 @@ packages: peerDependencies: postcss: ^8.2.15 - postcss-custom-media@8.0.2: - resolution: {integrity: sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==} - engines: {node: ^12 || ^14 || >=16} + postcss-custom-media@12.0.1: + resolution: {integrity: sha512-66syE14+VeqkUf0rRX0bvbTCbNRJF132jD+ceo8th1dap2YJEAqpdh5uG98CE3IbgHT7m9XM0GIlOazNWqQdeA==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.3 + postcss: ^8.4 - postcss-custom-properties@12.1.11: - resolution: {integrity: sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==} - engines: {node: ^12 || ^14 || >=16} + postcss-custom-properties@15.0.1: + resolution: {integrity: sha512-cuyq8sd8dLY0GLbelz1KB8IMIoDECo6RVXMeHeXY2Uw3Q05k/d1GVITdaKLsheqrHbnxlwxzSRZQQ5u+rNtbMg==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 - postcss-custom-selectors@6.0.3: - resolution: {integrity: sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==} - engines: {node: ^12 || ^14 || >=16} + postcss-custom-selectors@9.0.1: + resolution: {integrity: sha512-2XBELy4DmdVKimChfaZ2id9u9CSGYQhiJ53SvlfBvMTzLMW2VxuMb9rHsMSQw9kRq/zSbhT5x13EaK8JSmK8KQ==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.3 + postcss: ^8.4 - postcss-dir-pseudo-class@6.0.5: - resolution: {integrity: sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==} - engines: {node: ^12 || ^14 || >=16} + postcss-dir-pseudo-class@10.0.0: + resolution: {integrity: sha512-DmtIzULpyC8XaH4b5AaUgt4Jic4QmrECqidNCdR7u7naQFdnxX80YI06u238a+ZVRXwURDxVzy0s/UQnWmpVeg==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 postcss-discard-comments@5.1.2: resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} @@ -4797,15 +5365,9 @@ packages: peerDependencies: postcss: ^8.2.15 - postcss-double-position-gradients@3.1.2: - resolution: {integrity: sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.2 - - postcss-env-function@4.0.6: - resolution: {integrity: sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==} - engines: {node: ^12 || ^14 || >=16} + postcss-double-position-gradients@7.0.0: + resolution: {integrity: sha512-Msr/dxj8Os7KLJE5Hdhvprwm3K5Zrh1KTY0eFN3ngPKNkej/Usy4BM9JQmqE6CLAkDpHoQVsi4snbL72CPt6qg==} + engines: {node: '>=20.19.0'} peerDependencies: postcss: ^8.4 @@ -4814,15 +5376,15 @@ packages: peerDependencies: postcss: ^8.1.4 - postcss-focus-visible@6.0.4: - resolution: {integrity: sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==} - engines: {node: ^12 || ^14 || >=16} + postcss-focus-visible@11.0.0: + resolution: {integrity: sha512-VG1a9kBKizUBWS66t5xyB4uLONBnvZLCmZXxT40FALu8EF0QgVZBYy5ApC0KhmpHsv+pvHMJHB3agKHwmocWjw==} + engines: {node: '>=20.19.0'} peerDependencies: postcss: ^8.4 - postcss-focus-within@5.0.4: - resolution: {integrity: sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==} - engines: {node: ^12 || ^14 || >=16} + postcss-focus-within@10.0.0: + resolution: {integrity: sha512-dvql0fzUTG+gcJYp+KTbag5vAjuo94LDYZHkqDV1rnf5gPGer1v/SrmIZBdvKU8moep3HbcbujqGjzSb3DL53Q==} + engines: {node: '>=20.19.0'} peerDependencies: postcss: ^8.4 @@ -4831,34 +5393,29 @@ packages: peerDependencies: postcss: ^8.1.0 - postcss-gap-properties@3.0.5: - resolution: {integrity: sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==} - engines: {node: ^12 || ^14 || >=16} + postcss-gap-properties@7.0.0: + resolution: {integrity: sha512-PSDF2QoZMRUbsINvXObQgxx4HExRP85QTT8qS/YN9fBsCPWCqUuwqAD6E6PNp0BqL/jU1eyWUBORaOK/J/9LDA==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 - postcss-image-set-function@4.0.7: - resolution: {integrity: sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==} - engines: {node: ^12 || ^14 || >=16} + postcss-image-set-function@8.0.0: + resolution: {integrity: sha512-rEGNkOkNusf4+IuMmfEoIdLuVmvbExGbmG+MIsyV6jR5UaWSoyPcAYHV/PxzVDCmudyF+2Nh/o6Ub2saqUdnuA==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 - postcss-import@15.1.0: - resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} - engines: {node: '>=14.0.0'} + postcss-import@16.1.1: + resolution: {integrity: sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ==} + engines: {node: '>=18.0.0'} peerDependencies: postcss: ^8.0.0 - postcss-initial@4.0.1: - resolution: {integrity: sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==} + postcss-lab-function@8.0.3: + resolution: {integrity: sha512-rUa27RLVXjMn1aDkHEt5dRsK80+bAACPr8w5Ow0BkIlfH6gEk0Mh1I0REkYhtp4UhKFw1HLEk3AzvKBi6BGOqw==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.0.0 - - postcss-lab-function@4.2.1: - resolution: {integrity: sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.2 + postcss: ^8.4 postcss-load-config@3.1.4: resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} @@ -4890,18 +5447,12 @@ packages: yaml: optional: true - postcss-logical@5.0.4: - resolution: {integrity: sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==} - engines: {node: ^12 || ^14 || >=16} + postcss-logical@9.0.0: + resolution: {integrity: sha512-A4LNd9dk3q/juEUA9Gd8ALhBO3TeOeYurnyHLlf2aAToD94VHR8c5Uv7KNmf8YVRhTxvWsyug4c5fKtARzyIRQ==} + engines: {node: '>=20.19.0'} peerDependencies: postcss: ^8.4 - postcss-media-minmax@5.0.0: - resolution: {integrity: sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==} - engines: {node: '>=10.0.0'} - peerDependencies: - postcss: ^8.1.0 - postcss-merge-longhand@5.1.7: resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} engines: {node: ^10 || ^12 || >=14.0} @@ -4967,11 +5518,11 @@ packages: peerDependencies: postcss: ^8.0.0 - postcss-nesting@10.2.0: - resolution: {integrity: sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==} - engines: {node: ^12 || ^14 || >=16} + postcss-nesting@14.0.0: + resolution: {integrity: sha512-YGFOfVrjxYfeGTS5XctP1WCI5hu8Lr9SmntjfRC+iX5hCihEO+QZl9Ra+pkjqkgoVdDKvb2JccpElcowhZtzpw==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 postcss-normalize-charset@5.1.0: resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} @@ -5027,11 +5578,11 @@ packages: peerDependencies: postcss: ^8.2.15 - postcss-opacity-percentage@1.1.3: - resolution: {integrity: sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==} - engines: {node: ^12 || ^14 || >=16} + postcss-opacity-percentage@3.0.0: + resolution: {integrity: sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==} + engines: {node: '>=18'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 postcss-ordered-values@5.1.3: resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} @@ -5039,34 +5590,34 @@ packages: peerDependencies: postcss: ^8.2.15 - postcss-overflow-shorthand@3.0.4: - resolution: {integrity: sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==} - engines: {node: ^12 || ^14 || >=16} + postcss-overflow-shorthand@7.0.0: + resolution: {integrity: sha512-9SLpjoUdGRoRrzoOdX66HbUs0+uDwfIAiXsRa7piKGOqPd6F4ZlON9oaDSP5r1Qpgmzw5L9Ht0undIK6igJPMA==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 postcss-page-break@3.0.4: resolution: {integrity: sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==} peerDependencies: postcss: ^8 - postcss-place@7.0.5: - resolution: {integrity: sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==} - engines: {node: ^12 || ^14 || >=16} + postcss-place@11.0.0: + resolution: {integrity: sha512-fAifpyjQ+fuDRp2nmF95WbotqbpjdazebedahXdfBxy5sHembOLpBQ1cHveZD9ZmjK26tYM8tikeNaUlp/KfHA==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 - postcss-preset-env@7.8.3: - resolution: {integrity: sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==} - engines: {node: ^12 || ^14 || >=16} + postcss-preset-env@11.2.1: + resolution: {integrity: sha512-dqL7WR5wg9yP/+6pTHIsIeIpK6XVghJDE4/r4ZSdr5ExrbLiN5x78gly0Xs0MLGbHy2oT3WWNfbxowmnw9BurQ==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 - postcss-pseudo-class-any-link@7.1.6: - resolution: {integrity: sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==} - engines: {node: ^12 || ^14 || >=16} + postcss-pseudo-class-any-link@11.0.0: + resolution: {integrity: sha512-DNFZ4GMa3C3pU5dM+UCTG1CEeLtS1ZqV5DKSqCTJQMn1G5jnd/30fS8+A7H4o5bSD3MOcnx+VgI+xPE9Z5Wvig==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 postcss-reduce-initial@5.1.2: resolution: {integrity: sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==} @@ -5085,11 +5636,11 @@ packages: peerDependencies: postcss: ^8.0.3 - postcss-selector-not@6.0.1: - resolution: {integrity: sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==} - engines: {node: ^12 || ^14 || >=16} + postcss-selector-not@9.0.0: + resolution: {integrity: sha512-xhAtTdHnVU2M/CrpYOPyRUvg3njhVlKmn2GNYXDaRJV9Ygx4d5OkSkc7NINzjUqnbDFtaKXlISOBeyMXU/zyFQ==} + engines: {node: '>=20.19.0'} peerDependencies: - postcss: ^8.2 + postcss: ^8.4 postcss-selector-parser@6.1.2: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} @@ -5099,6 +5650,10 @@ packages: resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} engines: {node: '>=4'} + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + postcss-svgo@5.1.0: resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} engines: {node: ^10 || ^12 || >=14.0} @@ -5159,14 +5714,14 @@ packages: resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} engines: {node: '>=6'} - pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - pretty-format@30.0.5: resolution: {integrity: sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + pretty-format@30.3.0: + resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + prisma@7.7.0: resolution: {integrity: sha512-HlgwRBt1uEFB9LStHL4HLYDvoi4BNu1rYA0hPG0zCAEyK9SaZBqp7E5Rjpc3Qh8Lex/ye/svoHZ0OWoFNhWxuQ==} engines: {node: ^20.19 || ^22.12 || >=24.0} @@ -5204,8 +5759,8 @@ packages: proxy-from-env@1.0.0: resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==} - pump@3.0.3: - resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} @@ -5213,8 +5768,11 @@ packages: pure-rand@7.0.1: resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + pure-rand@8.4.0: + resolution: {integrity: sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==} + + qs@6.14.2: + resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} engines: {node: '>=0.6'} queue-microtask@1.2.3: @@ -5223,9 +5781,6 @@ packages: raf-schd@4.0.3: resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} @@ -5246,10 +5801,10 @@ packages: peerDependencies: react: ^19.2.5 - react-error-boundary@4.1.2: - resolution: {integrity: sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==} + react-error-boundary@6.1.1: + resolution: {integrity: sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==} peerDependencies: - react: '>=16.13.1' + react: ^18.0.0 || ^19.0.0 react-hook-form@7.71.1: resolution: {integrity: sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==} @@ -5288,6 +5843,13 @@ packages: react: ^16.8.0 || 17.x react-dom: ^16.8.0 || 17.x + react-simple-maps@3.0.0: + resolution: {integrity: sha512-vKNFrvpPG8Vyfdjnz5Ne1N56rZlDfHXv5THNXOVZMqbX1rWZA48zQuYT03mx6PAKanqarJu/PDLgshIZAfHHqw==} + peerDependencies: + prop-types: ^15.7.2 + react: ^16.8.0 || 17.x || 18.x + react-dom: ^16.8.0 || 17.x || 18.x + react-stately@3.46.0: resolution: {integrity: sha512-OdxhWvHgs2L4OJGIs7hnuTr5WjjMM6enhNEAMRqiekhF8+ITvA2LRwNftOZwcogaoCslGYq5S2VQTQwnm0GbCA==} peerDependencies: @@ -5309,6 +5871,12 @@ packages: react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-window@2.2.7: + resolution: {integrity: sha512-SH5nvfUQwGHYyriDUAOt7wfPsfG9Qxd6OdzQxl5oQ4dsSsUicqQvjV7dR+NqZ4coY0fUn3w1jnC5PwzIUWEg5w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + react@19.2.5: resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} engines: {node: '>=0.10.0'} @@ -5327,8 +5895,9 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - redis@4.7.1: - resolution: {integrity: sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==} + redis@5.12.1: + resolution: {integrity: sha512-LDsoVvb/CpoV9EN3FXvgvSHNJWuCIzl9MiO3ppOevuGLpSGJhwfQjpEwfFJcQvNSddHADDdZaWx0HnmMxRXG7g==} + engines: {node: '>= 18.19.0'} redux@5.0.1: resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} @@ -5373,15 +5942,16 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve.exports@2.0.3: - resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} - engines: {node: '>=10'} - resolve@1.22.10: resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} engines: {node: '>= 0.4'} hasBin: true + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} @@ -5397,11 +5967,6 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - rollup-plugin-copy@3.5.0: resolution: {integrity: sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==} engines: {node: '>=8.3'} @@ -5419,11 +5984,17 @@ packages: rollup: ^3.29.4 || ^4 typescript: ^4.5 || ^5.0 || ^6.0 - rollup-plugin-node-externals@8.1.2: - resolution: {integrity: sha512-EuB6/lolkMLK16gvibUjikERq5fCRVIGwD2xue/CrM8D0pz5GXD2V6N8IrgxegwbcUoKkUFI8VYCEEv8MMvgpA==} - engines: {node: '>= 21 || ^20.6.0 || ^18.19.0'} + rollup-plugin-node-externals@9.0.1: + resolution: {integrity: sha512-Yp91CX1ebDA8s4+BY7bOOdhF8VRR1XAKnSsHeVhYg4DW4MRXPsF4F2FnSsN9ghJgFhHl3L1uyK7A9NA/SF4Zgw==} + engines: {node: '>= 24'} peerDependencies: rollup: ^4.0.0 + vite: '>=5.0.0' + peerDependenciesMeta: + rollup: + optional: true + vite: + optional: true rollup-plugin-peer-deps-external@2.2.4: resolution: {integrity: sha512-AWdukIM1+k5JDdAqV/Cxd+nejvno2FVLVeZ74NKggm3Q5s9cbbcOgUPGdbxPi4BXu7xGaZ8HG12F+thImYu/0g==} @@ -5509,8 +6080,13 @@ packages: resolution: {integrity: sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw==} engines: {node: '>=18'} - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + serialize-error@13.0.1: + resolution: {integrity: sha512-bBZaRwLH9PN5HbLCjPId4dP5bNGEtumcErgOX952IsvOhVPrm3/AeK1y0UHA/QaPG701eg0yEnOKsCOC6X/kaA==} + engines: {node: '>=20'} + + serialize-javascript@7.0.5: + resolution: {integrity: sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==} + engines: {node: '>=20.0.0'} set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} @@ -5593,8 +6169,9 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} - smob@1.5.0: - resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==} + smob@1.6.1: + resolution: {integrity: sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==} + engines: {node: '>=20.0.0'} snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} @@ -5698,8 +6275,8 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.2: - resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} strip-bom@3.0.0: @@ -5774,6 +6351,20 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + synckit@0.11.12: + resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} + engines: {node: ^14.18.0 || >=16.0.0} + + systeminformation@5.31.5: + resolution: {integrity: sha512-5SyLdip4/3alxD4Kh+63bUQTJmu7YMfYQTC+koZy7X73HgNqZSD2P4wOZQWtUncvPvcEmnfIjCoygN4MRoEejQ==} + engines: {node: '>=8.0.0'} + os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] + hasBin: true + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + tailwind-merge@3.4.0: resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} @@ -5794,8 +6385,8 @@ packages: resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==} engines: {node: '>=18'} - terser@5.43.1: - resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} + terser@5.46.1: + resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==} engines: {node: '>=10'} hasBin: true @@ -5840,6 +6431,10 @@ packages: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + tldts-core@6.1.86: resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} @@ -5900,8 +6495,8 @@ packages: jest-util: optional: true - ts-morph@27.0.2: - resolution: {integrity: sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==} + ts-morph@28.0.0: + resolution: {integrity: sha512-Wp3tnZ2bzwxyTZMtgWVzXDfm7lB1Drz+y9DmmYH/L702PQhPyVrp3pkou3yIz4qjS14GY9kcpmLiOOMvl8oG1g==} ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} @@ -5917,6 +6512,9 @@ packages: '@swc/wasm': optional: true + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -5958,10 +6556,18 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + type-fest@4.41.0: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + type-fest@5.6.0: + resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} + engines: {node: '>=20'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -5978,8 +6584,8 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true @@ -6002,8 +6608,8 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} @@ -6017,6 +6623,9 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + untildify@4.0.0: resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} engines: {node: '>=8'} @@ -6027,21 +6636,17 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - use-intl@4.8.4: - resolution: {integrity: sha512-H4Hs0LdyKowskCHRuPajOPx4X+UZZije4GPgIcX9N+U87Y4z4Vpfl7sp9OmhJvCUoTr3HOeUiNvjiqEKBYlsCw==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-intl@4.9.1: + resolution: {integrity: sha512-iGVV/xFYlhe3btafRlL8RPLD2Jsuet4yqn9DR6LWWbMhULsJnXgLonDkzDmsAIBIwFtk02oJuX/Ox2vwHKF+UQ==} peerDependencies: react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 - use-memo-one@1.1.3: - resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - - use-sync-external-store@1.5.0: - resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -6138,9 +6743,9 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - write-file-atomic@4.0.2: - resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} @@ -6153,9 +6758,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -6233,6 +6835,8 @@ snapshots: '@babel/compat-data@7.28.0': {} + '@babel/compat-data@7.29.0': {} + '@babel/core@7.28.3': dependencies: '@ampproject/remapping': 2.3.0 @@ -6253,6 +6857,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@8.1.1) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/generator@7.28.3': dependencies: '@babel/parser': 7.28.3 @@ -6261,6 +6885,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.30 jsesc: 3.1.0 + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/helper-compilation-targets@7.27.2': dependencies: '@babel/compat-data': 7.28.0 @@ -6269,6 +6901,14 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + '@babel/helper-globals@7.28.0': {} '@babel/helper-module-imports@7.27.1': @@ -6278,6 +6918,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.3)': dependencies: '@babel/core': 7.28.3 @@ -6287,7 +6934,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.27.1': {} + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} '@babel/helper-string-parser@7.27.1': {} @@ -6302,103 +6958,120 @@ snapshots: '@babel/template': 7.27.2 '@babel/types': 7.28.2 + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + '@babel/parser@7.28.3': dependencies: '@babel/types': 7.28.2 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.3)': + '@babel/parser@7.29.2': dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/types': 7.29.0 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.3)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.3)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.3)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.3)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.3)': + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.3)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.3)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.3)': + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.3)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.3)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.3)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.3)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.3)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.3)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.3)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.3)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 '@babel/runtime@7.28.3': {} + '@babel/runtime@7.29.2': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 '@babel/parser': 7.28.3 '@babel/types': 7.28.2 + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@babel/traverse@7.28.3': dependencies: '@babel/code-frame': 7.27.1 @@ -6411,11 +7084,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + '@babel/types@7.28.2': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@bcoe/v8-coverage@0.2.3': {} '@biomejs/biome@2.4.12': @@ -6470,92 +7160,331 @@ snapshots: dependencies: '@clickhouse/client-common': 1.18.2 - '@colors/colors@1.5.0': - optional: true - '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@csstools/postcss-cascade-layers@1.1.1(postcss@8.5.10)': + '@csstools/cascade-layer-name-parser@3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: - '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.2) - postcss: 8.5.10 - postcss-selector-parser: 6.1.2 + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 - '@csstools/postcss-color-function@1.1.1(postcss@8.5.10)': + '@csstools/color-helpers@6.0.2': {} + + '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.10) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-tokenizer@4.0.0': {} + + '@csstools/media-query-list-parser@5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/postcss-alpha-function@2.0.4(postcss@8.5.10)': + dependencies: + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10) + '@csstools/utilities': 3.0.0(postcss@8.5.10) + postcss: 8.5.10 + + '@csstools/postcss-cascade-layers@6.0.0(postcss@8.5.10)': + dependencies: + '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1) + postcss: 8.5.10 + postcss-selector-parser: 7.1.1 + + '@csstools/postcss-color-function-display-p3-linear@2.0.3(postcss@8.5.10)': + dependencies: + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10) + '@csstools/utilities': 3.0.0(postcss@8.5.10) + postcss: 8.5.10 + + '@csstools/postcss-color-function@5.0.3(postcss@8.5.10)': + dependencies: + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10) + '@csstools/utilities': 3.0.0(postcss@8.5.10) + postcss: 8.5.10 + + '@csstools/postcss-color-mix-function@4.0.3(postcss@8.5.10)': + dependencies: + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10) + '@csstools/utilities': 3.0.0(postcss@8.5.10) + postcss: 8.5.10 + + '@csstools/postcss-color-mix-variadic-function-arguments@2.0.3(postcss@8.5.10)': + dependencies: + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10) + '@csstools/utilities': 3.0.0(postcss@8.5.10) + postcss: 8.5.10 + + '@csstools/postcss-content-alt-text@3.0.0(postcss@8.5.10)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10) + '@csstools/utilities': 3.0.0(postcss@8.5.10) + postcss: 8.5.10 + + '@csstools/postcss-contrast-color-function@3.0.3(postcss@8.5.10)': + dependencies: + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10) + '@csstools/utilities': 3.0.0(postcss@8.5.10) + postcss: 8.5.10 + + '@csstools/postcss-exponential-functions@3.0.2(postcss@8.5.10)': + dependencies: + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + postcss: 8.5.10 + + '@csstools/postcss-font-format-keywords@5.0.0(postcss@8.5.10)': + dependencies: + '@csstools/utilities': 3.0.0(postcss@8.5.10) postcss: 8.5.10 postcss-value-parser: 4.2.0 - '@csstools/postcss-font-format-keywords@1.0.1(postcss@8.5.10)': + '@csstools/postcss-font-width-property@1.0.0(postcss@8.5.10)': + dependencies: + '@csstools/utilities': 3.0.0(postcss@8.5.10) + postcss: 8.5.10 + + '@csstools/postcss-gamut-mapping@3.0.3(postcss@8.5.10)': + dependencies: + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + postcss: 8.5.10 + + '@csstools/postcss-gradients-interpolation-method@6.0.3(postcss@8.5.10)': + dependencies: + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10) + '@csstools/utilities': 3.0.0(postcss@8.5.10) + postcss: 8.5.10 + + '@csstools/postcss-hwb-function@5.0.3(postcss@8.5.10)': + dependencies: + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10) + '@csstools/utilities': 3.0.0(postcss@8.5.10) + postcss: 8.5.10 + + '@csstools/postcss-ic-unit@5.0.0(postcss@8.5.10)': + dependencies: + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10) + '@csstools/utilities': 3.0.0(postcss@8.5.10) + postcss: 8.5.10 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-initial@3.0.0(postcss@8.5.10)': + dependencies: + postcss: 8.5.10 + + '@csstools/postcss-is-pseudo-class@6.0.0(postcss@8.5.10)': + dependencies: + '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1) + postcss: 8.5.10 + postcss-selector-parser: 7.1.1 + + '@csstools/postcss-light-dark-function@3.0.0(postcss@8.5.10)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10) + '@csstools/utilities': 3.0.0(postcss@8.5.10) + postcss: 8.5.10 + + '@csstools/postcss-logical-float-and-clear@4.0.0(postcss@8.5.10)': + dependencies: + postcss: 8.5.10 + + '@csstools/postcss-logical-overflow@3.0.0(postcss@8.5.10)': + dependencies: + postcss: 8.5.10 + + '@csstools/postcss-logical-overscroll-behavior@3.0.0(postcss@8.5.10)': + dependencies: + postcss: 8.5.10 + + '@csstools/postcss-logical-resize@4.0.0(postcss@8.5.10)': dependencies: postcss: 8.5.10 postcss-value-parser: 4.2.0 - '@csstools/postcss-hwb-function@1.0.2(postcss@8.5.10)': + '@csstools/postcss-logical-viewport-units@4.0.0(postcss@8.5.10)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + '@csstools/utilities': 3.0.0(postcss@8.5.10) + postcss: 8.5.10 + + '@csstools/postcss-media-minmax@3.0.2(postcss@8.5.10)': + dependencies: + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + postcss: 8.5.10 + + '@csstools/postcss-media-queries-aspect-ratio-number-values@4.0.0(postcss@8.5.10)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + postcss: 8.5.10 + + '@csstools/postcss-mixins@1.0.0(postcss@8.5.10)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + postcss: 8.5.10 + + '@csstools/postcss-nested-calc@5.0.0(postcss@8.5.10)': + dependencies: + '@csstools/utilities': 3.0.0(postcss@8.5.10) + postcss: 8.5.10 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-normalize-display-values@5.0.1(postcss@8.5.10)': dependencies: postcss: 8.5.10 postcss-value-parser: 4.2.0 - '@csstools/postcss-ic-unit@1.0.1(postcss@8.5.10)': + '@csstools/postcss-oklab-function@5.0.3(postcss@8.5.10)': dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.10) + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10) + '@csstools/utilities': 3.0.0(postcss@8.5.10) postcss: 8.5.10 - postcss-value-parser: 4.2.0 - '@csstools/postcss-is-pseudo-class@2.0.7(postcss@8.5.10)': + '@csstools/postcss-position-area-property@2.0.0(postcss@8.5.10)': dependencies: - '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.2) postcss: 8.5.10 - postcss-selector-parser: 6.1.2 - '@csstools/postcss-nested-calc@1.0.0(postcss@8.5.10)': + '@csstools/postcss-progressive-custom-properties@5.0.0(postcss@8.5.10)': dependencies: postcss: 8.5.10 postcss-value-parser: 4.2.0 - '@csstools/postcss-normalize-display-values@1.0.1(postcss@8.5.10)': + '@csstools/postcss-property-rule-prelude-list@2.0.0(postcss@8.5.10)': dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + postcss: 8.5.10 + + '@csstools/postcss-random-function@3.0.2(postcss@8.5.10)': + dependencies: + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + postcss: 8.5.10 + + '@csstools/postcss-relative-color-syntax@4.0.3(postcss@8.5.10)': + dependencies: + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10) + '@csstools/utilities': 3.0.0(postcss@8.5.10) + postcss: 8.5.10 + + '@csstools/postcss-scope-pseudo-class@5.0.0(postcss@8.5.10)': + dependencies: + postcss: 8.5.10 + postcss-selector-parser: 7.1.1 + + '@csstools/postcss-sign-functions@2.0.2(postcss@8.5.10)': + dependencies: + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + postcss: 8.5.10 + + '@csstools/postcss-stepped-value-functions@5.0.2(postcss@8.5.10)': + dependencies: + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + postcss: 8.5.10 + + '@csstools/postcss-syntax-descriptor-syntax-production@2.0.0(postcss@8.5.10)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + postcss: 8.5.10 + + '@csstools/postcss-system-ui-font-family@2.0.0(postcss@8.5.10)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + postcss: 8.5.10 + + '@csstools/postcss-text-decoration-shorthand@5.0.3(postcss@8.5.10)': + dependencies: + '@csstools/color-helpers': 6.0.2 postcss: 8.5.10 postcss-value-parser: 4.2.0 - '@csstools/postcss-oklab-function@1.1.1(postcss@8.5.10)': + '@csstools/postcss-trigonometric-functions@5.0.2(postcss@8.5.10)': dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.10) + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 postcss: 8.5.10 - postcss-value-parser: 4.2.0 - '@csstools/postcss-progressive-custom-properties@1.3.0(postcss@8.5.10)': - dependencies: - postcss: 8.5.10 - postcss-value-parser: 4.2.0 - - '@csstools/postcss-stepped-value-functions@1.0.1(postcss@8.5.10)': - dependencies: - postcss: 8.5.10 - postcss-value-parser: 4.2.0 - - '@csstools/postcss-text-decoration-shorthand@1.0.0(postcss@8.5.10)': - dependencies: - postcss: 8.5.10 - postcss-value-parser: 4.2.0 - - '@csstools/postcss-trigonometric-functions@1.0.2(postcss@8.5.10)': - dependencies: - postcss: 8.5.10 - postcss-value-parser: 4.2.0 - - '@csstools/postcss-unset-value@1.0.2(postcss@8.5.10)': + '@csstools/postcss-unset-value@5.0.0(postcss@8.5.10)': dependencies: postcss: 8.5.10 - '@csstools/selector-specificity@2.2.0(postcss-selector-parser@6.1.2)': + '@csstools/selector-resolve-nested@4.0.0(postcss-selector-parser@7.1.1)': dependencies: - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.1.1 - '@cypress/request@3.0.9': + '@csstools/selector-specificity@6.0.0(postcss-selector-parser@7.1.1)': + dependencies: + postcss-selector-parser: 7.1.1 + + '@csstools/utilities@3.0.0(postcss@8.5.10)': + dependencies: + postcss: 8.5.10 + + '@cypress/request@3.0.10': dependencies: aws-sign2: 0.7.0 aws4: 1.13.2 @@ -6563,14 +7492,14 @@ snapshots: combined-stream: 1.0.8 extend: 3.0.2 forever-agent: 0.6.1 - form-data: 4.0.4 + form-data: 4.0.5 http-signature: 1.4.0 is-typedarray: 1.0.0 isstream: 0.1.2 json-stringify-safe: 5.0.1 mime-types: 2.1.35 performance-now: 2.1.0 - qs: 6.14.0 + qs: 6.14.2 safe-buffer: 5.2.1 tough-cookie: 5.1.2 tunnel-agent: 0.6.0 @@ -6583,7 +7512,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@date-fns/utc@1.2.0': {} + '@date-fns/utc@2.1.1': {} '@dicebear/adventurer-neutral@9.4.2(@dicebear/core@9.4.2)': dependencies: @@ -6758,125 +7687,206 @@ snapshots: '@electric-sql/pglite@0.4.1': {} + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.9.2': dependencies: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@epic-web/invariant@1.0.0': {} '@esbuild/aix-ppc64@0.27.7': optional: true + '@esbuild/aix-ppc64@0.28.0': + optional: true + '@esbuild/android-arm64@0.27.7': optional: true + '@esbuild/android-arm64@0.28.0': + optional: true + '@esbuild/android-arm@0.27.7': optional: true + '@esbuild/android-arm@0.28.0': + optional: true + '@esbuild/android-x64@0.27.7': optional: true + '@esbuild/android-x64@0.28.0': + optional: true + '@esbuild/darwin-arm64@0.27.7': optional: true + '@esbuild/darwin-arm64@0.28.0': + optional: true + '@esbuild/darwin-x64@0.27.7': optional: true + '@esbuild/darwin-x64@0.28.0': + optional: true + '@esbuild/freebsd-arm64@0.27.7': optional: true + '@esbuild/freebsd-arm64@0.28.0': + optional: true + '@esbuild/freebsd-x64@0.27.7': optional: true + '@esbuild/freebsd-x64@0.28.0': + optional: true + '@esbuild/linux-arm64@0.27.7': optional: true + '@esbuild/linux-arm64@0.28.0': + optional: true + '@esbuild/linux-arm@0.27.7': optional: true + '@esbuild/linux-arm@0.28.0': + optional: true + '@esbuild/linux-ia32@0.27.7': optional: true + '@esbuild/linux-ia32@0.28.0': + optional: true + '@esbuild/linux-loong64@0.27.7': optional: true + '@esbuild/linux-loong64@0.28.0': + optional: true + '@esbuild/linux-mips64el@0.27.7': optional: true + '@esbuild/linux-mips64el@0.28.0': + optional: true + '@esbuild/linux-ppc64@0.27.7': optional: true + '@esbuild/linux-ppc64@0.28.0': + optional: true + '@esbuild/linux-riscv64@0.27.7': optional: true + '@esbuild/linux-riscv64@0.28.0': + optional: true + '@esbuild/linux-s390x@0.27.7': optional: true + '@esbuild/linux-s390x@0.28.0': + optional: true + '@esbuild/linux-x64@0.27.7': optional: true + '@esbuild/linux-x64@0.28.0': + optional: true + '@esbuild/netbsd-arm64@0.27.7': optional: true + '@esbuild/netbsd-arm64@0.28.0': + optional: true + '@esbuild/netbsd-x64@0.27.7': optional: true + '@esbuild/netbsd-x64@0.28.0': + optional: true + '@esbuild/openbsd-arm64@0.27.7': optional: true + '@esbuild/openbsd-arm64@0.28.0': + optional: true + '@esbuild/openbsd-x64@0.27.7': optional: true + '@esbuild/openbsd-x64@0.28.0': + optional: true + '@esbuild/openharmony-arm64@0.27.7': optional: true + '@esbuild/openharmony-arm64@0.28.0': + optional: true + '@esbuild/sunos-x64@0.27.7': optional: true + '@esbuild/sunos-x64@0.28.0': + optional: true + '@esbuild/win32-arm64@0.27.7': optional: true + '@esbuild/win32-arm64@0.28.0': + optional: true + '@esbuild/win32-ia32@0.27.7': optional: true + '@esbuild/win32-ia32@0.28.0': + optional: true + '@esbuild/win32-x64@0.27.7': optional: true - '@formatjs/bigdecimal@0.2.0': {} + '@esbuild/win32-x64@0.28.0': + optional: true - '@formatjs/ecma402-abstract@3.2.0': + '@formatjs/fast-memoize@3.1.2': {} + + '@formatjs/icu-messageformat-parser@3.5.4': dependencies: - '@formatjs/bigdecimal': 0.2.0 - '@formatjs/fast-memoize': 3.1.1 - '@formatjs/intl-localematcher': 0.8.2 + '@formatjs/icu-skeleton-parser': 2.1.4 - '@formatjs/fast-memoize@3.1.1': {} + '@formatjs/icu-skeleton-parser@2.1.4': {} - '@formatjs/icu-messageformat-parser@3.5.3': + '@formatjs/intl-localematcher@0.8.3': dependencies: - '@formatjs/ecma402-abstract': 3.2.0 - '@formatjs/icu-skeleton-parser': 2.1.3 + '@formatjs/fast-memoize': 3.1.2 - '@formatjs/icu-skeleton-parser@2.1.3': + '@hello-pangea/dnd@18.0.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@formatjs/ecma402-abstract': 3.2.0 - - '@formatjs/intl-localematcher@0.8.2': - dependencies: - '@formatjs/fast-memoize': 3.1.1 - - '@hello-pangea/dnd@17.0.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.29.2 css-box-model: 1.2.1 - memoize-one: 6.0.0 raf-schd: 4.0.3 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1) redux: 5.0.1 - use-memo-one: 1.1.3(react@19.2.5) transitivePeerDependencies: - '@types/react' @@ -6997,17 +8007,11 @@ snapshots: dependencies: '@swc/helpers': 0.5.21 - '@isaacs/balanced-match@4.0.1': {} - - '@isaacs/brace-expansion@5.0.0': - dependencies: - '@isaacs/balanced-match': 4.0.1 - '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 strip-ansi-cjs: strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 @@ -7021,200 +8025,204 @@ snapshots: camelcase: 5.3.1 find-up: 4.1.0 get-package-type: 0.1.0 - js-yaml: 3.14.1 + js-yaml: 3.14.2 resolve-from: 5.0.0 - '@istanbuljs/schema@0.1.3': {} + '@istanbuljs/schema@0.1.6': {} - '@jest/console@29.7.0': + '@jest/console@30.3.0': dependencies: - '@jest/types': 29.6.3 - '@types/node': 24.12.2 + '@jest/types': 30.3.0 + '@types/node': 25.6.0 chalk: 4.1.2 - jest-message-util: 29.7.0 - jest-util: 29.7.0 + jest-message-util: 30.3.0 + jest-util: 30.3.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3))': + '@jest/core@30.3.0(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3))': dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 24.12.2 + '@jest/console': 30.3.0 + '@jest/pattern': 30.0.1 + '@jest/reporters': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 25.6.0 ansi-escapes: 4.3.2 chalk: 4.1.2 - ci-info: 3.9.0 - exit: 0.1.2 + ci-info: 4.4.0 + exit-x: 0.2.2 graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.8 - pretty-format: 29.7.0 + jest-changed-files: 30.3.0 + jest-config: 30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)) + jest-haste-map: 30.3.0 + jest-message-util: 30.3.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.3.0 + jest-resolve-dependencies: 30.3.0 + jest-runner: 30.3.0 + jest-runtime: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 + jest-watcher: 30.3.0 + pretty-format: 30.3.0 slash: 3.0.0 - strip-ansi: 6.0.1 transitivePeerDependencies: - babel-plugin-macros + - esbuild-register - supports-color - ts-node '@jest/diff-sequences@30.0.1': {} - '@jest/environment@29.7.0': - dependencies: - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 24.12.2 - jest-mock: 29.7.0 + '@jest/diff-sequences@30.3.0': {} - '@jest/expect-utils@29.7.0': + '@jest/environment@30.3.0': dependencies: - jest-get-type: 29.6.3 + '@jest/fake-timers': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 25.6.0 + jest-mock: 30.3.0 '@jest/expect-utils@30.1.1': dependencies: '@jest/get-type': 30.1.0 - '@jest/expect@29.7.0': + '@jest/expect-utils@30.3.0': dependencies: - expect: 29.7.0 - jest-snapshot: 29.7.0 + '@jest/get-type': 30.1.0 + + '@jest/expect@30.3.0': + dependencies: + expect: 30.3.0 + jest-snapshot: 30.3.0 transitivePeerDependencies: - supports-color - '@jest/fake-timers@29.7.0': + '@jest/fake-timers@30.3.0': dependencies: - '@jest/types': 29.6.3 - '@sinonjs/fake-timers': 10.3.0 - '@types/node': 24.12.2 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-util: 29.7.0 + '@jest/types': 30.3.0 + '@sinonjs/fake-timers': 15.3.2 + '@types/node': 25.6.0 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-util: 30.3.0 '@jest/get-type@30.1.0': {} - '@jest/globals@29.7.0': + '@jest/globals@30.3.0': dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/types': 29.6.3 - jest-mock: 29.7.0 + '@jest/environment': 30.3.0 + '@jest/expect': 30.3.0 + '@jest/types': 30.3.0 + jest-mock: 30.3.0 transitivePeerDependencies: - supports-color '@jest/pattern@30.0.1': dependencies: - '@types/node': 24.12.2 + '@types/node': 25.6.0 jest-regex-util: 30.0.1 - '@jest/reporters@29.7.0': + '@jest/reporters@30.3.0': dependencies: '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.30 - '@types/node': 24.12.2 + '@jest/console': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 25.6.0 chalk: 4.1.2 - collect-v8-coverage: 1.0.2 - exit: 0.1.2 - glob: 7.2.3 + collect-v8-coverage: 1.0.3 + exit-x: 0.2.2 + glob: 10.5.0 graceful-fs: 4.2.11 istanbul-lib-coverage: 3.2.2 istanbul-lib-instrument: 6.0.3 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.1.7 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - jest-worker: 29.7.0 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + jest-message-util: 30.3.0 + jest-util: 30.3.0 + jest-worker: 30.3.0 slash: 3.0.0 string-length: 4.0.2 - strip-ansi: 6.0.1 v8-to-istanbul: 9.3.0 transitivePeerDependencies: - supports-color - '@jest/schemas@29.6.3': - dependencies: - '@sinclair/typebox': 0.27.8 - '@jest/schemas@30.0.5': dependencies: '@sinclair/typebox': 0.34.40 - '@jest/source-map@29.6.3': + '@jest/snapshot-utils@30.3.0': dependencies: - '@jridgewell/trace-mapping': 0.3.30 + '@jest/types': 30.3.0 + chalk: 4.1.2 + graceful-fs: 4.2.11 + natural-compare: 1.4.0 + + '@jest/source-map@30.0.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 callsites: 3.1.0 graceful-fs: 4.2.11 - '@jest/test-result@29.7.0': + '@jest/test-result@30.3.0': dependencies: - '@jest/console': 29.7.0 - '@jest/types': 29.6.3 + '@jest/console': 30.3.0 + '@jest/types': 30.3.0 '@types/istanbul-lib-coverage': 2.0.6 - collect-v8-coverage: 1.0.2 + collect-v8-coverage: 1.0.3 - '@jest/test-sequencer@29.7.0': + '@jest/test-sequencer@30.3.0': dependencies: - '@jest/test-result': 29.7.0 + '@jest/test-result': 30.3.0 graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 + jest-haste-map: 30.3.0 slash: 3.0.0 - '@jest/transform@29.7.0': + '@jest/transform@30.3.0': dependencies: - '@babel/core': 7.28.3 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.30 - babel-plugin-istanbul: 6.1.1 + '@babel/core': 7.29.0 + '@jest/types': 30.3.0 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 7.0.1 chalk: 4.1.2 convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - micromatch: 4.0.8 + jest-haste-map: 30.3.0 + jest-regex-util: 30.0.1 + jest-util: 30.3.0 pirates: 4.0.7 slash: 3.0.0 - write-file-atomic: 4.0.2 + write-file-atomic: 5.0.1 transitivePeerDependencies: - supports-color - '@jest/types@29.6.3': - dependencies: - '@jest/schemas': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 3.0.4 - '@types/node': 24.12.2 - '@types/yargs': 17.0.33 - chalk: 4.1.2 - '@jest/types@30.0.5': dependencies: '@jest/pattern': 30.0.1 '@jest/schemas': 30.0.5 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 24.12.2 + '@types/node': 25.6.0 '@types/yargs': 17.0.33 chalk: 4.1.2 + '@jest/types@30.3.0': + dependencies: + '@jest/pattern': 30.0.1 + '@jest/schemas': 30.0.5 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 25.6.0 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -7230,7 +8238,7 @@ snapshots: '@jridgewell/source-map@0.3.11': dependencies: '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/sourcemap-codec@1.5.5': {} @@ -7251,6 +8259,13 @@ snapshots: '@kurkle/color@0.3.4': {} + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.1 + optional: true + '@netlify/plugin-nextjs@5.15.9': {} '@next/env@16.2.4': {} @@ -7335,7 +8350,7 @@ snapshots: detect-libc: 2.1.2 is-glob: 4.0.3 node-addon-api: 7.1.1 - picomatch: 4.0.3 + picomatch: 4.0.4 optionalDependencies: '@parcel/watcher-android-arm64': 2.5.6 '@parcel/watcher-darwin-arm64': 2.5.6 @@ -7354,6 +8369,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@pkgr/core@0.2.9': {} + '@prisma/adapter-pg@7.7.0': dependencies: '@prisma/driver-adapter-utils': 7.7.0 @@ -7365,12 +8382,12 @@ snapshots: '@prisma/client-runtime-utils@7.7.0': {} - '@prisma/client@7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)': + '@prisma/client@7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3))(typescript@6.0.3)': dependencies: '@prisma/client-runtime-utils': 7.7.0 optionalDependencies: - prisma: 7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3) - typescript: 5.9.3 + prisma: 7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3) + typescript: 6.0.3 '@prisma/config@7.7.0': dependencies: @@ -7385,7 +8402,7 @@ snapshots: '@prisma/debug@7.7.0': {} - '@prisma/dev@0.24.3(typescript@5.9.3)': + '@prisma/dev@0.24.3(typescript@6.0.3)': dependencies: '@electric-sql/pglite': 0.4.1 '@electric-sql/pglite-socket': 0.1.1(@electric-sql/pglite@0.4.1) @@ -7402,7 +8419,7 @@ snapshots: proper-lockfile: 4.1.2 remeda: 2.33.4 std-env: 3.10.0 - valibot: 1.2.0(typescript@5.9.3) + valibot: 1.2.0(typescript@6.0.3) zeptomatch: 2.1.0 transitivePeerDependencies: - typescript @@ -7420,9 +8437,9 @@ snapshots: '@prisma/fetch-engine': 7.7.0 '@prisma/get-platform': 7.7.0 - '@prisma/extension-read-replicas@0.5.0(@prisma/client@7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3))': + '@prisma/extension-read-replicas@0.5.0(@prisma/client@7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3))(typescript@6.0.3))': dependencies: - '@prisma/client': 7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3) + '@prisma/client': 7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3))(typescript@6.0.3) '@prisma/fetch-engine@7.7.0': dependencies: @@ -7630,44 +8647,39 @@ snapshots: dependencies: react: 19.2.5 - '@redis/bloom@1.2.0(@redis/client@1.6.1)': + '@redis/bloom@5.12.1(@redis/client@5.12.1)': dependencies: - '@redis/client': 1.6.1 + '@redis/client': 5.12.1 - '@redis/client@1.6.1': + '@redis/client@5.12.1': dependencies: cluster-key-slot: 1.1.2 - generic-pool: 3.9.0 - yallist: 4.0.0 - '@redis/graph@1.1.1(@redis/client@1.6.1)': + '@redis/json@5.12.1(@redis/client@5.12.1)': dependencies: - '@redis/client': 1.6.1 + '@redis/client': 5.12.1 - '@redis/json@1.0.7(@redis/client@1.6.1)': + '@redis/search@5.12.1(@redis/client@5.12.1)': dependencies: - '@redis/client': 1.6.1 + '@redis/client': 5.12.1 - '@redis/search@1.2.0(@redis/client@1.6.1)': + '@redis/time-series@5.12.1(@redis/client@5.12.1)': dependencies: - '@redis/client': 1.6.1 + '@redis/client': 5.12.1 - '@redis/time-series@1.1.0(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - - '@rollup/plugin-alias@5.1.1(rollup@4.60.1)': + '@rollup/plugin-alias@6.0.0(rollup@4.60.1)': optionalDependencies: rollup: 4.60.1 - '@rollup/plugin-commonjs@25.0.8(rollup@4.60.1)': + '@rollup/plugin-commonjs@29.0.2(rollup@4.60.1)': dependencies: - '@rollup/pluginutils': 5.2.0(rollup@4.60.1) + '@rollup/pluginutils': 5.3.0(rollup@4.60.1) commondir: 1.0.1 estree-walker: 2.0.2 - glob: 8.1.0 + fdir: 6.5.0(picomatch@4.0.4) is-reference: 1.2.1 - magic-string: 0.30.17 + magic-string: 0.30.21 + picomatch: 4.0.4 optionalDependencies: rollup: 4.60.1 @@ -7677,36 +8689,36 @@ snapshots: optionalDependencies: rollup: 4.60.1 - '@rollup/plugin-node-resolve@15.3.1(rollup@4.60.1)': + '@rollup/plugin-node-resolve@16.0.3(rollup@4.60.1)': dependencies: - '@rollup/pluginutils': 5.2.0(rollup@4.60.1) + '@rollup/pluginutils': 5.3.0(rollup@4.60.1) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 - resolve: 1.22.10 + resolve: 1.22.12 optionalDependencies: rollup: 4.60.1 - '@rollup/plugin-replace@5.0.7(rollup@4.60.1)': + '@rollup/plugin-replace@6.0.3(rollup@4.60.1)': dependencies: - '@rollup/pluginutils': 5.2.0(rollup@4.60.1) - magic-string: 0.30.17 + '@rollup/pluginutils': 5.3.0(rollup@4.60.1) + magic-string: 0.30.21 optionalDependencies: rollup: 4.60.1 - '@rollup/plugin-terser@0.4.4(rollup@4.60.1)': + '@rollup/plugin-terser@1.0.0(rollup@4.60.1)': dependencies: - serialize-javascript: 6.0.2 - smob: 1.5.0 - terser: 5.43.1 + serialize-javascript: 7.0.5 + smob: 1.6.1 + terser: 5.46.1 optionalDependencies: rollup: 4.60.1 - '@rollup/plugin-typescript@12.3.0(rollup@4.60.1)(tslib@2.8.1)(typescript@5.9.3)': + '@rollup/plugin-typescript@12.3.0(rollup@4.60.1)(tslib@2.8.1)(typescript@6.0.3)': dependencies: '@rollup/pluginutils': 5.2.0(rollup@4.60.1) resolve: 1.22.10 - typescript: 5.9.3 + typescript: 6.0.3 optionalDependencies: rollup: 4.60.1 tslib: 2.8.1 @@ -7719,6 +8731,14 @@ snapshots: optionalDependencies: rollup: 4.60.1 + '@rollup/pluginutils@5.3.0(rollup@4.60.1)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.4 + optionalDependencies: + rollup: 4.60.1 + '@rollup/rollup-android-arm-eabi@4.60.1': optional: true @@ -7798,8 +8818,6 @@ snapshots: '@schummar/icu-type-parser@1.21.5': {} - '@sinclair/typebox@0.27.8': {} - '@sinclair/typebox@0.34.40': {} '@sindresorhus/merge-streams@2.3.0': {} @@ -7808,7 +8826,7 @@ snapshots: dependencies: type-detect: 4.0.8 - '@sinonjs/fake-timers@10.3.0': + '@sinonjs/fake-timers@15.3.2': dependencies: '@sinonjs/commons': 3.0.1 @@ -7858,12 +8876,12 @@ snapshots: '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.3) '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.3) - '@svgr/cli@8.1.0(typescript@5.9.3)': + '@svgr/cli@8.1.0(typescript@6.0.3)': dependencies: - '@svgr/core': 8.1.0(typescript@5.9.3) - '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) - '@svgr/plugin-prettier': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) - '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3) + '@svgr/core': 8.1.0(typescript@6.0.3) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@6.0.3)) + '@svgr/plugin-prettier': 8.1.0(@svgr/core@8.1.0(typescript@6.0.3)) + '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@6.0.3))(typescript@6.0.3) camelcase: 6.3.0 chalk: 4.1.2 commander: 9.5.0 @@ -7874,12 +8892,12 @@ snapshots: - supports-color - typescript - '@svgr/core@8.1.0(typescript@5.9.3)': + '@svgr/core@8.1.0(typescript@6.0.3)': dependencies: '@babel/core': 7.28.3 '@svgr/babel-preset': 8.1.0(@babel/core@7.28.3) camelcase: 6.3.0 - cosmiconfig: 8.3.6(typescript@5.9.3) + cosmiconfig: 8.3.6(typescript@6.0.3) snake-case: 3.0.4 transitivePeerDependencies: - supports-color @@ -7890,76 +8908,84 @@ snapshots: '@babel/types': 7.28.2 entities: 4.5.0 - '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))': + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@6.0.3))': dependencies: '@babel/core': 7.28.3 '@svgr/babel-preset': 8.1.0(@babel/core@7.28.3) - '@svgr/core': 8.1.0(typescript@5.9.3) + '@svgr/core': 8.1.0(typescript@6.0.3) '@svgr/hast-util-to-babel-ast': 8.0.0 svg-parser: 2.0.4 transitivePeerDependencies: - supports-color - '@svgr/plugin-prettier@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))': + '@svgr/plugin-prettier@8.1.0(@svgr/core@8.1.0(typescript@6.0.3))': dependencies: - '@svgr/core': 8.1.0(typescript@5.9.3) + '@svgr/core': 8.1.0(typescript@6.0.3) deepmerge: 4.3.1 prettier: 2.8.8 - '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3)': + '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@6.0.3))(typescript@6.0.3)': dependencies: - '@svgr/core': 8.1.0(typescript@5.9.3) - cosmiconfig: 8.3.6(typescript@5.9.3) + '@svgr/core': 8.1.0(typescript@6.0.3) + cosmiconfig: 8.3.6(typescript@6.0.3) deepmerge: 4.3.1 svgo: 3.3.2 transitivePeerDependencies: - typescript - '@swc/core-darwin-arm64@1.15.11': + '@swc/core-darwin-arm64@1.15.26': optional: true - '@swc/core-darwin-x64@1.15.11': + '@swc/core-darwin-x64@1.15.26': optional: true - '@swc/core-linux-arm-gnueabihf@1.15.11': + '@swc/core-linux-arm-gnueabihf@1.15.26': optional: true - '@swc/core-linux-arm64-gnu@1.15.11': + '@swc/core-linux-arm64-gnu@1.15.26': optional: true - '@swc/core-linux-arm64-musl@1.15.11': + '@swc/core-linux-arm64-musl@1.15.26': optional: true - '@swc/core-linux-x64-gnu@1.15.11': + '@swc/core-linux-ppc64-gnu@1.15.26': optional: true - '@swc/core-linux-x64-musl@1.15.11': + '@swc/core-linux-s390x-gnu@1.15.26': optional: true - '@swc/core-win32-arm64-msvc@1.15.11': + '@swc/core-linux-x64-gnu@1.15.26': optional: true - '@swc/core-win32-ia32-msvc@1.15.11': + '@swc/core-linux-x64-musl@1.15.26': optional: true - '@swc/core-win32-x64-msvc@1.15.11': + '@swc/core-win32-arm64-msvc@1.15.26': optional: true - '@swc/core@1.15.11(@swc/helpers@0.5.21)': + '@swc/core-win32-ia32-msvc@1.15.26': + optional: true + + '@swc/core-win32-x64-msvc@1.15.26': + optional: true + + '@swc/core@1.15.26(@swc/helpers@0.5.21)': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.25 + '@swc/types': 0.1.26 optionalDependencies: - '@swc/core-darwin-arm64': 1.15.11 - '@swc/core-darwin-x64': 1.15.11 - '@swc/core-linux-arm-gnueabihf': 1.15.11 - '@swc/core-linux-arm64-gnu': 1.15.11 - '@swc/core-linux-arm64-musl': 1.15.11 - '@swc/core-linux-x64-gnu': 1.15.11 - '@swc/core-linux-x64-musl': 1.15.11 - '@swc/core-win32-arm64-msvc': 1.15.11 - '@swc/core-win32-ia32-msvc': 1.15.11 - '@swc/core-win32-x64-msvc': 1.15.11 + '@swc/core-darwin-arm64': 1.15.26 + '@swc/core-darwin-x64': 1.15.26 + '@swc/core-linux-arm-gnueabihf': 1.15.26 + '@swc/core-linux-arm64-gnu': 1.15.26 + '@swc/core-linux-arm64-musl': 1.15.26 + '@swc/core-linux-ppc64-gnu': 1.15.26 + '@swc/core-linux-s390x-gnu': 1.15.26 + '@swc/core-linux-x64-gnu': 1.15.26 + '@swc/core-linux-x64-musl': 1.15.26 + '@swc/core-win32-arm64-msvc': 1.15.26 + '@swc/core-win32-ia32-msvc': 1.15.26 + '@swc/core-win32-x64-msvc': 1.15.26 '@swc/helpers': 0.5.21 '@swc/counter@0.1.3': {} @@ -7976,7 +9002,7 @@ snapshots: dependencies: tslib: 2.8.1 - '@swc/types@0.1.25': + '@swc/types@0.1.26': dependencies: '@swc/counter': 0.1.3 @@ -7989,11 +9015,11 @@ snapshots: '@trysound/sax@0.2.0': {} - '@ts-morph/common@0.28.1': + '@ts-morph/common@0.29.0': dependencies: - minimatch: 10.0.3 + minimatch: 10.2.5 path-browserify: 1.0.1 - tinyglobby: 0.2.14 + tinyglobby: 0.2.16 '@tsconfig/node10@1.0.11': {} @@ -8005,26 +9031,31 @@ snapshots: '@tsconfig/svelte@1.0.13': {} + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.29.0 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.29.0 '@types/css-font-loading-module@0.0.7': {} @@ -8032,16 +9063,12 @@ snapshots: '@types/fs-extra@8.1.5': dependencies: - '@types/node': 24.12.2 + '@types/node': 25.6.0 '@types/glob@7.2.0': dependencies: '@types/minimatch': 6.0.0 - '@types/node': 24.12.2 - - '@types/graceful-fs@4.1.9': - dependencies: - '@types/node': 24.12.2 + '@types/node': 25.6.0 '@types/istanbul-lib-coverage@2.0.6': {} @@ -8062,15 +9089,15 @@ snapshots: '@types/minimatch@6.0.0': dependencies: - minimatch: 10.0.3 + minimatch: 10.2.5 - '@types/node@24.12.2': + '@types/node@25.6.0': dependencies: - undici-types: 7.16.0 + undici-types: 7.19.2 '@types/pg@8.20.0': dependencies: - '@types/node': 24.12.2 + '@types/node': 25.6.0 pg-protocol: 1.13.0 pg-types: 2.2.0 @@ -8086,10 +9113,12 @@ snapshots: '@types/sinonjs__fake-timers@8.1.1': {} - '@types/sizzle@2.3.9': {} + '@types/sizzle@2.3.10': {} '@types/stack-utils@2.0.3': {} + '@types/tmp@0.2.6': {} + '@types/use-sync-external-store@0.0.6': {} '@types/yargs-parser@21.0.3': {} @@ -8098,9 +9127,13 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + '@types/yauzl@2.10.3': dependencies: - '@types/node': 24.12.2 + '@types/node': 25.6.0 optional: true '@umami/react-zen@0.245.0(@types/react@19.2.14)(immer@11.1.4)(react-aria-components@1.17.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.1.18)(use-sync-external-store@1.6.0(react@19.2.5))': @@ -8123,6 +9156,67 @@ snapshots: - tailwindcss - use-sync-external-store + '@ungap/structured-clone@1.3.0': {} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + '@xstate/fsm@1.6.5': {} acorn-walk@8.3.4: @@ -8131,6 +9225,8 @@ snapshots: acorn@8.15.0: {} + acorn@8.16.0: {} + aggregate-error@3.1.0: dependencies: clean-stack: 2.2.0 @@ -8170,7 +9266,7 @@ snapshots: anymatch@3.1.3: dependencies: normalize-path: 3.0.0 - picomatch: 2.3.1 + picomatch: 2.3.2 arch@2.2.0: {} @@ -8213,18 +9309,15 @@ snapshots: async-function@1.0.0: {} - async@3.2.6: {} - asynckit@0.4.0: {} at-least-node@1.0.0: {} - autoprefixer@10.4.21(postcss@8.5.10): + autoprefixer@10.5.0(postcss@8.5.10): dependencies: - browserslist: 4.25.2 - caniuse-lite: 1.0.30001780 - fraction.js: 4.3.7 - normalize-range: 0.1.2 + browserslist: 4.28.2 + caniuse-lite: 1.0.30001788 + fraction.js: 5.3.4 picocolors: 1.1.1 postcss: 8.5.10 postcss-value-parser: 4.2.0 @@ -8239,67 +9332,66 @@ snapshots: aws4@1.13.2: {} - babel-jest@29.7.0(@babel/core@7.28.3): + babel-jest@30.3.0(@babel/core@7.29.0): dependencies: - '@babel/core': 7.28.3 - '@jest/transform': 29.7.0 + '@babel/core': 7.29.0 + '@jest/transform': 30.3.0 '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.28.3) + babel-plugin-istanbul: 7.0.1 + babel-preset-jest: 30.3.0(@babel/core@7.29.0) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 transitivePeerDependencies: - supports-color - babel-plugin-istanbul@6.1.1: + babel-plugin-istanbul@7.0.1: dependencies: - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-instrument: 5.2.1 + '@istanbuljs/schema': 0.1.6 + istanbul-lib-instrument: 6.0.3 test-exclude: 6.0.0 transitivePeerDependencies: - supports-color - babel-plugin-jest-hoist@29.6.3: + babel-plugin-jest-hoist@30.3.0: dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.2 '@types/babel__core': 7.20.5 - '@types/babel__traverse': 7.28.0 babel-plugin-react-compiler@19.1.0-rc.2: dependencies: '@babel/types': 7.28.2 - babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.3): + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): dependencies: - '@babel/core': 7.28.3 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.3) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.3) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.3) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.3) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.3) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.3) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.3) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.3) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.3) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.3) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.3) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.3) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.3) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.3) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.3) + '@babel/core': 7.29.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) - babel-preset-jest@29.6.3(@babel/core@7.28.3): + babel-preset-jest@30.3.0(@babel/core@7.29.0): dependencies: - '@babel/core': 7.28.3 - babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.3) + '@babel/core': 7.29.0 + babel-plugin-jest-hoist: 30.3.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + base64-arraybuffer@1.0.2: {} base64-js@1.5.1: {} @@ -8327,10 +9419,19 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -8342,6 +9443,14 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.2) + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.19 + caniuse-lite: 1.0.30001788 + electron-to-chromium: 1.5.340 + node-releases: 2.0.37 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + bs-logger@0.2.6: dependencies: fast-json-stable-stringify: 2.1.0 @@ -8440,12 +9549,10 @@ snapshots: dependencies: '@kurkle/color': 0.3.4 - chartjs-adapter-date-fns@3.0.0(chart.js@4.5.1)(date-fns@2.30.0): + chartjs-adapter-date-fns@3.0.0(chart.js@4.5.1)(date-fns@4.1.0): dependencies: chart.js: 4.5.1 - date-fns: 2.30.0 - - check-more-types@2.24.0: {} + date-fns: 4.1.0 chokidar@4.0.3: dependencies: @@ -8453,17 +9560,17 @@ snapshots: chownr@3.0.0: {} - ci-info@3.9.0: {} - ci-info@4.3.0: {} + ci-info@4.4.0: {} + citty@0.1.6: dependencies: consola: 3.4.2 citty@0.2.2: {} - cjs-module-lexer@1.4.3: {} + cjs-module-lexer@2.2.0: {} classnames@2.5.1: {} @@ -8473,11 +9580,11 @@ snapshots: dependencies: restore-cursor: 3.1.0 - cli-table3@0.6.5: + cli-table3@0.6.1: dependencies: string-width: 4.2.3 optionalDependencies: - '@colors/colors': 1.5.0 + colors: 1.4.0 cli-truncate@2.1.0: dependencies: @@ -8500,7 +9607,7 @@ snapshots: code-block-writer@13.0.3: {} - collect-v8-coverage@1.0.2: {} + collect-v8-coverage@1.0.3: {} color-convert@1.9.3: dependencies: @@ -8520,6 +9627,9 @@ snapshots: colorette@2.0.20: {} + colors@1.4.0: + optional: true + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -8561,29 +9671,14 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 - cosmiconfig@8.3.6(typescript@5.9.3): + cosmiconfig@8.3.6(typescript@6.0.3): dependencies: import-fresh: 3.3.1 js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: - typescript: 5.9.3 - - create-jest@29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)): - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node + typescript: 6.0.3 create-require@1.1.1: {} @@ -8606,10 +9701,10 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-blank-pseudo@3.0.3(postcss@8.5.10): + css-blank-pseudo@8.0.1(postcss@8.5.10): dependencies: postcss: 8.5.10 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.1.1 css-box-model@1.2.1: dependencies: @@ -8619,12 +9714,14 @@ snapshots: dependencies: postcss: 8.5.10 - css-has-pseudo@3.0.4(postcss@8.5.10): + css-has-pseudo@8.0.0(postcss@8.5.10): dependencies: + '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1) postcss: 8.5.10 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 - css-prefers-color-scheme@6.0.3(postcss@8.5.10): + css-prefers-color-scheme@11.0.0(postcss@8.5.10): dependencies: postcss: 8.5.10 @@ -8661,7 +9758,7 @@ snapshots: css-what@6.2.2: {} - cssdb@7.11.2: {} + cssdb@8.8.0: {} cssesc@3.0.0: {} @@ -8719,25 +9816,25 @@ snapshots: csstype@3.2.3: {} - cypress@13.17.0: + cypress@15.14.0: dependencies: - '@cypress/request': 3.0.9 + '@cypress/request': 3.0.10 '@cypress/xvfb': 1.2.4(supports-color@8.1.1) '@types/sinonjs__fake-timers': 8.1.1 - '@types/sizzle': 2.3.9 + '@types/sizzle': 2.3.10 + '@types/tmp': 0.2.6 arch: 2.2.0 blob-util: 2.0.2 bluebird: 3.7.2 buffer: 5.7.1 cachedir: 2.4.0 chalk: 4.1.2 - check-more-types: 2.24.0 - ci-info: 4.3.0 + ci-info: 4.4.0 cli-cursor: 3.1.0 - cli-table3: 0.6.5 + cli-table3: 0.6.1 commander: 6.2.1 common-tags: 1.8.2 - dayjs: 1.11.13 + dayjs: 1.11.20 debug: 4.4.3(supports-color@8.1.1) enquirer: 2.4.1 eventemitter2: 6.4.7 @@ -8746,11 +9843,10 @@ snapshots: extract-zip: 2.0.1(supports-color@8.1.1) figures: 3.2.0 fs-extra: 9.1.0 - getos: 3.2.1 + hasha: 5.2.2 is-installed-globally: 0.4.0 - lazy-ass: 1.6.0 listr2: 3.14.0(enquirer@2.4.1) - lodash: 4.17.21 + lodash: 4.18.1 log-symbols: 4.1.0 minimist: 1.2.8 ospath: 1.2.2 @@ -8758,10 +9854,11 @@ snapshots: process: 0.11.10 proxy-from-env: 1.0.0 request-progress: 3.0.0 - semver: 7.7.4 supports-color: 8.1.1 + systeminformation: 5.31.5 tmp: 0.2.5 tree-kill: 1.2.2 + tslib: 1.14.1 untildify: 4.0.0 yauzl: 2.10.0 @@ -8835,15 +9932,13 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - date-fns-tz@1.3.8(date-fns@2.30.0): + date-fns-tz@3.2.0(date-fns@4.1.0): dependencies: - date-fns: 2.30.0 + date-fns: 4.1.0 - date-fns@2.30.0: - dependencies: - '@babel/runtime': 7.28.3 + date-fns@4.1.0: {} - dayjs@1.11.13: {} + dayjs@1.11.20: {} debug@3.2.7(supports-color@8.1.1): dependencies: @@ -8857,7 +9952,7 @@ snapshots: optionalDependencies: supports-color: 8.1.1 - dedent@1.6.0: {} + dedent@1.7.2: {} deepmerge-ts@7.1.5: {} @@ -8877,17 +9972,6 @@ snapshots: defu@6.1.7: {} - del@6.1.1: - dependencies: - globby: 11.1.0 - graceful-fs: 4.2.11 - is-glob: 4.0.3 - is-path-cwd: 2.2.0 - is-path-inside: 3.0.3 - p-map: 4.0.0 - rimraf: 3.0.2 - slash: 3.0.0 - del@8.0.1: dependencies: globby: 14.1.0 @@ -8912,8 +9996,6 @@ snapshots: detect-newline@3.1.0: {} - diff-sequences@29.6.3: {} - diff@4.0.2: {} dir-glob@3.0.1: @@ -8998,6 +10080,8 @@ snapshots: electron-to-chromium@1.5.202: {} + electron-to-chromium@1.5.340: {} + emittery@0.13.1: {} emoji-regex@8.0.0: {} @@ -9132,6 +10216,35 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 + esbuild@0.28.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 + escalade@3.2.0: {} escape-string-regexp@1.0.5: {} @@ -9176,15 +10289,7 @@ snapshots: dependencies: pify: 2.3.0 - exit@0.1.2: {} - - expect@29.7.0: - dependencies: - '@jest/expect-utils': 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 + exit-x@0.2.2: {} expect@30.1.1: dependencies: @@ -9195,6 +10300,15 @@ snapshots: jest-mock: 30.0.5 jest-util: 30.0.5 + expect@30.3.0: + dependencies: + '@jest/expect-utils': 30.3.0 + '@jest/get-type': 30.1.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-util: 30.3.0 + exsolve@1.0.8: {} extend@3.0.2: {} @@ -9241,9 +10355,9 @@ snapshots: dependencies: pend: 1.2.0 - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 fetch-blob@3.2.0: dependencies: @@ -9282,19 +10396,19 @@ snapshots: forever-agent@0.6.1: {} - form-data@4.0.4: + form-data@4.0.5: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 - hasown: 2.0.2 + hasown: 2.0.3 mime-types: 2.1.35 formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 - fraction.js@4.3.7: {} + fraction.js@5.3.4: {} fs-extra@8.1.0: dependencies: @@ -9335,8 +10449,6 @@ snapshots: dependencies: loader-utils: 3.3.1 - generic-pool@3.9.0: {} - gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -9365,7 +10477,7 @@ snapshots: get-stream@5.2.0: dependencies: - pump: 3.0.3 + pump: 3.0.4 get-stream@6.0.1: {} @@ -9379,10 +10491,6 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - getos@3.2.1: - dependencies: - async: 3.2.6 - getpass@0.1.7: dependencies: assert-plus: 1.0.0 @@ -9446,15 +10554,6 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 - globby@11.1.0: - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.3 - ignore: 5.3.2 - merge2: 1.4.1 - slash: 3.0.0 - globby@14.1.0: dependencies: '@sindresorhus/merge-streams': 2.3.0 @@ -9501,10 +10600,19 @@ snapshots: dependencies: has-symbols: 1.1.0 + hasha@5.2.2: + dependencies: + is-stream: 2.0.1 + type-fest: 0.8.1 + hasown@2.0.2: dependencies: function-bind: 1.1.2 + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + hono@4.12.9: {} hosted-git-info@2.8.9: {} @@ -9533,9 +10641,9 @@ snapshots: dependencies: postcss: 8.5.10 - icu-minify@4.8.4: + icu-minify@4.9.1: dependencies: - '@formatjs/icu-messageformat-parser': 3.5.3 + '@formatjs/icu-messageformat-parser': 3.5.4 ieee754@1.2.1: {} @@ -9586,11 +10694,10 @@ snapshots: internmap@1.0.1: {} - intl-messageformat@11.2.0: + intl-messageformat@11.2.1: dependencies: - '@formatjs/ecma402-abstract': 3.2.0 - '@formatjs/fast-memoize': 3.1.1 - '@formatjs/icu-messageformat-parser': 3.5.3 + '@formatjs/fast-memoize': 3.1.2 + '@formatjs/icu-messageformat-parser': 3.5.4 ipaddr.js@2.3.0: {} @@ -9621,9 +10728,9 @@ snapshots: is-callable@1.2.7: {} - is-ci@3.0.1: + is-ci@4.1.0: dependencies: - ci-info: 3.9.0 + ci-info: 4.4.0 is-core-module@2.16.1: dependencies: @@ -9640,7 +10747,7 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-docker@3.0.0: {} + is-docker@4.0.0: {} is-extglob@2.1.1: {} @@ -9668,7 +10775,7 @@ snapshots: global-dirs: 3.0.1 is-path-inside: 3.0.3 - is-localhost-ip@2.0.0: {} + is-localhost-ip@3.0.1: {} is-map@2.0.3: {} @@ -9683,8 +10790,6 @@ snapshots: is-number@7.0.0: {} - is-path-cwd@2.2.0: {} - is-path-cwd@3.0.0: {} is-path-inside@3.0.3: {} @@ -9758,21 +10863,11 @@ snapshots: istanbul-lib-coverage@3.2.2: {} - istanbul-lib-instrument@5.2.1: - dependencies: - '@babel/core': 7.28.3 - '@babel/parser': 7.28.3 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.2 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - istanbul-lib-instrument@6.0.3: dependencies: - '@babel/core': 7.28.3 - '@babel/parser': 7.28.3 - '@istanbuljs/schema': 0.1.3 + '@babel/core': 7.29.0 + '@babel/parser': 7.29.2 + '@istanbuljs/schema': 0.1.6 istanbul-lib-coverage: 3.2.2 semver: 7.7.4 transitivePeerDependencies: @@ -9784,15 +10879,15 @@ snapshots: make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@4.0.1: + istanbul-lib-source-maps@5.0.6: dependencies: + '@jridgewell/trace-mapping': 0.3.31 debug: 4.4.3(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 - source-map: 0.6.1 transitivePeerDependencies: - supports-color - istanbul-reports@3.1.7: + istanbul-reports@3.2.0: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 @@ -9803,95 +10898,89 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jest-changed-files@29.7.0: + jest-changed-files@30.3.0: dependencies: execa: 5.1.1 - jest-util: 29.7.0 + jest-util: 30.3.0 p-limit: 3.1.0 - jest-circus@29.7.0: + jest-circus@30.3.0: dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 24.12.2 + '@jest/environment': 30.3.0 + '@jest/expect': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 25.6.0 chalk: 4.1.2 co: 4.6.0 - dedent: 1.6.0 + dedent: 1.7.2 is-generator-fn: 2.1.0 - jest-each: 29.7.0 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 + jest-each: 30.3.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-runtime: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 p-limit: 3.1.0 - pretty-format: 29.7.0 - pure-rand: 6.1.0 + pretty-format: 30.3.0 + pure-rand: 7.0.1 slash: 3.0.0 stack-utils: 2.0.6 transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)): + jest-cli@30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 + '@jest/core': 30.3.0(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)) + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) - exit: 0.1.2 + exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) - jest-util: 29.7.0 - jest-validate: 29.7.0 + jest-config: 30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)) + jest-util: 30.3.0 + jest-validate: 30.3.0 yargs: 17.7.2 transitivePeerDependencies: - '@types/node' - babel-plugin-macros + - esbuild-register - supports-color - ts-node - jest-config@29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)): + jest-config@30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)): dependencies: - '@babel/core': 7.28.3 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.3) + '@babel/core': 7.29.0 + '@jest/get-type': 30.1.0 + '@jest/pattern': 30.0.1 + '@jest/test-sequencer': 30.3.0 + '@jest/types': 30.3.0 + babel-jest: 30.3.0(@babel/core@7.29.0) chalk: 4.1.2 - ci-info: 3.9.0 + ci-info: 4.4.0 deepmerge: 4.3.1 - glob: 7.2.3 + glob: 10.5.0 graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 + jest-circus: 30.3.0 + jest-docblock: 30.2.0 + jest-environment-node: 30.3.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.3.0 + jest-runner: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 parse-json: 5.2.0 - pretty-format: 29.7.0 + pretty-format: 30.3.0 slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 24.12.2 - ts-node: 10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3) + '@types/node': 25.6.0 + ts-node: 10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-diff@29.7.0: - dependencies: - chalk: 4.1.2 - diff-sequences: 29.6.3 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - jest-diff@30.1.1: dependencies: '@jest/diff-sequences': 30.0.1 @@ -9899,56 +10988,54 @@ snapshots: chalk: 4.1.2 pretty-format: 30.0.5 - jest-docblock@29.7.0: + jest-diff@30.3.0: + dependencies: + '@jest/diff-sequences': 30.3.0 + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + pretty-format: 30.3.0 + + jest-docblock@30.2.0: dependencies: detect-newline: 3.1.0 - jest-each@29.7.0: + jest-each@30.3.0: dependencies: - '@jest/types': 29.6.3 + '@jest/get-type': 30.1.0 + '@jest/types': 30.3.0 chalk: 4.1.2 - jest-get-type: 29.6.3 - jest-util: 29.7.0 - pretty-format: 29.7.0 + jest-util: 30.3.0 + pretty-format: 30.3.0 - jest-environment-node@29.7.0: + jest-environment-node@30.3.0: dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 24.12.2 - jest-mock: 29.7.0 - jest-util: 29.7.0 + '@jest/environment': 30.3.0 + '@jest/fake-timers': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 25.6.0 + jest-mock: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 - jest-get-type@29.6.3: {} - - jest-haste-map@29.7.0: + jest-haste-map@30.3.0: dependencies: - '@jest/types': 29.6.3 - '@types/graceful-fs': 4.1.9 - '@types/node': 24.12.2 + '@jest/types': 30.3.0 + '@types/node': 25.6.0 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - jest-worker: 29.7.0 - micromatch: 4.0.8 + jest-regex-util: 30.0.1 + jest-util: 30.3.0 + jest-worker: 30.3.0 + picomatch: 4.0.4 walker: 1.0.8 optionalDependencies: fsevents: 2.3.3 - jest-leak-detector@29.7.0: + jest-leak-detector@30.3.0: dependencies: - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-matcher-utils@29.7.0: - dependencies: - chalk: 4.1.2 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 + '@jest/get-type': 30.1.0 + pretty-format: 30.3.0 jest-matcher-utils@30.1.1: dependencies: @@ -9957,17 +11044,12 @@ snapshots: jest-diff: 30.1.1 pretty-format: 30.0.5 - jest-message-util@29.7.0: + jest-matcher-utils@30.3.0: dependencies: - '@babel/code-frame': 7.29.0 - '@jest/types': 29.6.3 - '@types/stack-utils': 2.0.3 + '@jest/get-type': 30.1.0 chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - stack-utils: 2.0.6 + jest-diff: 30.3.0 + pretty-format: 30.3.0 jest-message-util@30.1.0: dependencies: @@ -9981,177 +11063,190 @@ snapshots: slash: 3.0.0 stack-utils: 2.0.6 - jest-mock@29.7.0: + jest-message-util@30.3.0: dependencies: - '@jest/types': 29.6.3 - '@types/node': 24.12.2 - jest-util: 29.7.0 + '@babel/code-frame': 7.29.0 + '@jest/types': 30.3.0 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + picomatch: 4.0.4 + pretty-format: 30.3.0 + slash: 3.0.0 + stack-utils: 2.0.6 jest-mock@30.0.5: dependencies: '@jest/types': 30.0.5 - '@types/node': 24.12.2 + '@types/node': 25.6.0 jest-util: 30.0.5 - jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): - optionalDependencies: - jest-resolve: 29.7.0 + jest-mock@30.3.0: + dependencies: + '@jest/types': 30.3.0 + '@types/node': 25.6.0 + jest-util: 30.3.0 - jest-regex-util@29.6.3: {} + jest-pnp-resolver@1.2.3(jest-resolve@30.3.0): + optionalDependencies: + jest-resolve: 30.3.0 jest-regex-util@30.0.1: {} - jest-resolve-dependencies@29.7.0: + jest-resolve-dependencies@30.3.0: dependencies: - jest-regex-util: 29.6.3 - jest-snapshot: 29.7.0 + jest-regex-util: 30.0.1 + jest-snapshot: 30.3.0 transitivePeerDependencies: - supports-color - jest-resolve@29.7.0: + jest-resolve@30.3.0: dependencies: chalk: 4.1.2 graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) - jest-util: 29.7.0 - jest-validate: 29.7.0 - resolve: 1.22.10 - resolve.exports: 2.0.3 + jest-haste-map: 30.3.0 + jest-pnp-resolver: 1.2.3(jest-resolve@30.3.0) + jest-util: 30.3.0 + jest-validate: 30.3.0 slash: 3.0.0 + unrs-resolver: 1.11.1 - jest-runner@29.7.0: + jest-runner@30.3.0: dependencies: - '@jest/console': 29.7.0 - '@jest/environment': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 24.12.2 + '@jest/console': 30.3.0 + '@jest/environment': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 25.6.0 chalk: 4.1.2 emittery: 0.13.1 + exit-x: 0.2.2 graceful-fs: 4.2.11 - jest-docblock: 29.7.0 - jest-environment-node: 29.7.0 - jest-haste-map: 29.7.0 - jest-leak-detector: 29.7.0 - jest-message-util: 29.7.0 - jest-resolve: 29.7.0 - jest-runtime: 29.7.0 - jest-util: 29.7.0 - jest-watcher: 29.7.0 - jest-worker: 29.7.0 + jest-docblock: 30.2.0 + jest-environment-node: 30.3.0 + jest-haste-map: 30.3.0 + jest-leak-detector: 30.3.0 + jest-message-util: 30.3.0 + jest-resolve: 30.3.0 + jest-runtime: 30.3.0 + jest-util: 30.3.0 + jest-watcher: 30.3.0 + jest-worker: 30.3.0 p-limit: 3.1.0 source-map-support: 0.5.13 transitivePeerDependencies: - supports-color - jest-runtime@29.7.0: + jest-runtime@30.3.0: dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/globals': 29.7.0 - '@jest/source-map': 29.6.3 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 24.12.2 + '@jest/environment': 30.3.0 + '@jest/fake-timers': 30.3.0 + '@jest/globals': 30.3.0 + '@jest/source-map': 30.0.1 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 25.6.0 chalk: 4.1.2 - cjs-module-lexer: 1.4.3 - collect-v8-coverage: 1.0.2 - glob: 7.2.3 + cjs-module-lexer: 2.2.0 + collect-v8-coverage: 1.0.3 + glob: 10.5.0 graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 + jest-haste-map: 30.3.0 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 slash: 3.0.0 strip-bom: 4.0.0 transitivePeerDependencies: - supports-color - jest-snapshot@29.7.0: + jest-snapshot@30.3.0: dependencies: - '@babel/core': 7.28.3 - '@babel/generator': 7.28.3 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.3) - '@babel/types': 7.28.2 - '@jest/expect-utils': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.3) + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 + '@jest/expect-utils': 30.3.0 + '@jest/get-type': 30.1.0 + '@jest/snapshot-utils': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) chalk: 4.1.2 - expect: 29.7.0 + expect: 30.3.0 graceful-fs: 4.2.11 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - natural-compare: 1.4.0 - pretty-format: 29.7.0 + jest-diff: 30.3.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-util: 30.3.0 + pretty-format: 30.3.0 semver: 7.7.4 + synckit: 0.11.12 transitivePeerDependencies: - supports-color - jest-util@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/node': 24.12.2 - chalk: 4.1.2 - ci-info: 3.9.0 - graceful-fs: 4.2.11 - picomatch: 2.3.1 - jest-util@30.0.5: dependencies: '@jest/types': 30.0.5 - '@types/node': 24.12.2 + '@types/node': 25.6.0 chalk: 4.1.2 ci-info: 4.3.0 graceful-fs: 4.2.11 picomatch: 4.0.3 - jest-validate@29.7.0: + jest-util@30.3.0: dependencies: - '@jest/types': 29.6.3 + '@jest/types': 30.3.0 + '@types/node': 25.6.0 + chalk: 4.1.2 + ci-info: 4.4.0 + graceful-fs: 4.2.11 + picomatch: 4.0.4 + + jest-validate@30.3.0: + dependencies: + '@jest/get-type': 30.1.0 + '@jest/types': 30.3.0 camelcase: 6.3.0 chalk: 4.1.2 - jest-get-type: 29.6.3 leven: 3.1.0 - pretty-format: 29.7.0 + pretty-format: 30.3.0 - jest-watcher@29.7.0: + jest-watcher@30.3.0: dependencies: - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 24.12.2 + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 25.6.0 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 - jest-util: 29.7.0 + jest-util: 30.3.0 string-length: 4.0.2 - jest-worker@29.7.0: + jest-worker@30.3.0: dependencies: - '@types/node': 24.12.2 - jest-util: 29.7.0 + '@types/node': 25.6.0 + '@ungap/structured-clone': 1.3.0 + jest-util: 30.3.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)): + jest@30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) - '@jest/types': 29.6.3 + '@jest/core': 30.3.0(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)) + '@jest/types': 30.3.0 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) + jest-cli: 30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros + - esbuild-register - supports-color - ts-node @@ -10161,7 +11256,7 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@3.14.1: + js-yaml@3.14.2: dependencies: argparse: 1.0.10 esprima: 4.0.1 @@ -10238,8 +11333,6 @@ snapshots: kleur@3.0.3: {} - lazy-ass@1.6.0: {} - leven@3.1.0: {} lie@3.3.0: @@ -10300,7 +11393,7 @@ snapshots: lodash.uniq@4.5.0: {} - lodash@4.17.21: {} + lodash@4.18.1: {} log-symbols@4.1.0: dependencies: @@ -10344,10 +11437,6 @@ snapshots: dependencies: react: 19.2.5 - magic-string@0.30.17: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -10377,8 +11466,6 @@ snapshots: memoize-one@5.2.1: {} - memoize-one@6.0.0: {} - memorystream@0.3.1: {} merge-stream@2.0.0: {} @@ -10388,7 +11475,7 @@ snapshots: micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.1 + picomatch: 2.3.2 mime-db@1.52.0: {} @@ -10398,14 +11485,18 @@ snapshots: mimic-fn@2.1.0: {} - minimatch@10.0.3: + minimatch@10.2.5: dependencies: - '@isaacs/brace-expansion': 5.0.0 + brace-expansion: 5.0.5 minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + minimatch@5.1.6: dependencies: brace-expansion: 2.0.2 @@ -10426,7 +11517,7 @@ snapshots: mlly@1.8.0: dependencies: - acorn: 8.15.0 + acorn: 8.16.0 pathe: 2.0.3 pkg-types: 1.3.1 ufo: 1.6.1 @@ -10459,32 +11550,34 @@ snapshots: nanoid@3.3.11: {} + napi-postinstall@0.3.4: {} + natural-compare@1.4.0: {} negotiator@1.0.0: {} neo-async@2.6.2: {} - next-intl-swc-plugin-extractor@4.8.4: {} + next-intl-swc-plugin-extractor@4.9.1: {} - next-intl@4.8.3(@swc/helpers@0.5.21)(next@16.2.4(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@5.9.3): + next-intl@4.9.1(@swc/helpers@0.5.21)(next@16.2.4(@babel/core@7.29.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@6.0.3): dependencies: - '@formatjs/intl-localematcher': 0.8.2 + '@formatjs/intl-localematcher': 0.8.3 '@parcel/watcher': 2.5.6 - '@swc/core': 1.15.11(@swc/helpers@0.5.21) - icu-minify: 4.8.4 + '@swc/core': 1.15.26(@swc/helpers@0.5.21) + icu-minify: 4.9.1 negotiator: 1.0.0 - next: 16.2.4(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - next-intl-swc-plugin-extractor: 4.8.4 + next: 16.2.4(@babel/core@7.29.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + next-intl-swc-plugin-extractor: 4.9.1 po-parser: 2.1.1 react: 19.2.5 - use-intl: 4.8.4(react@19.2.5) + use-intl: 4.9.1(react@19.2.5) optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - '@swc/helpers' - next@16.2.4(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + next@16.2.4(@babel/core@7.29.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@next/env': 16.2.4 '@swc/helpers': 0.5.15 @@ -10493,7 +11586,7 @@ snapshots: postcss: 8.4.31 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - styled-jsx: 5.1.6(@babel/core@7.28.3)(react@19.2.5) + styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.5) optionalDependencies: '@next/swc-darwin-arm64': 16.2.4 '@next/swc-darwin-x64': 16.2.4 @@ -10532,17 +11625,19 @@ snapshots: node-releases@2.0.19: {} + node-releases@2.0.37: {} + + non-error@0.1.0: {} + normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 - resolve: 1.22.10 + resolve: 1.22.12 semver: 5.7.2 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} - normalize-range@0.1.2: {} - normalize-url@6.1.0: {} npm-run-all@4.1.5: @@ -10727,10 +11822,12 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.1: {} + picomatch@2.3.2: {} picomatch@4.0.3: {} + picomatch@4.0.4: {} + pidtree@0.3.1: {} pify@2.3.0: {} @@ -10761,10 +11858,10 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-attribute-case-insensitive@5.0.2(postcss@8.5.10): + postcss-attribute-case-insensitive@8.0.0(postcss@8.5.10): dependencies: postcss: 8.5.10 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.1.1 postcss-calc@8.2.4(postcss@8.5.10): dependencies: @@ -10777,18 +11874,24 @@ snapshots: postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-color-functional-notation@4.2.4(postcss@8.5.10): + postcss-color-functional-notation@8.0.3(postcss@8.5.10): dependencies: + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10) + '@csstools/utilities': 3.0.0(postcss@8.5.10) + postcss: 8.5.10 + + postcss-color-hex-alpha@11.0.0(postcss@8.5.10): + dependencies: + '@csstools/utilities': 3.0.0(postcss@8.5.10) postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-color-hex-alpha@8.0.4(postcss@8.5.10): - dependencies: - postcss: 8.5.10 - postcss-value-parser: 4.2.0 - - postcss-color-rebeccapurple@7.1.1(postcss@8.5.10): + postcss-color-rebeccapurple@11.0.0(postcss@8.5.10): dependencies: + '@csstools/utilities': 3.0.0(postcss@8.5.10) postcss: 8.5.10 postcss-value-parser: 4.2.0 @@ -10806,25 +11909,35 @@ snapshots: postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-custom-media@8.0.2(postcss@8.5.10): + postcss-custom-media@12.0.1(postcss@8.5.10): dependencies: + '@csstools/cascade-layer-name-parser': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + postcss: 8.5.10 + + postcss-custom-properties@15.0.1(postcss@8.5.10): + dependencies: + '@csstools/cascade-layer-name-parser': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/utilities': 3.0.0(postcss@8.5.10) postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-custom-properties@12.1.11(postcss@8.5.10): + postcss-custom-selectors@9.0.1(postcss@8.5.10): dependencies: + '@csstools/cascade-layer-name-parser': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 postcss: 8.5.10 - postcss-value-parser: 4.2.0 + postcss-selector-parser: 7.1.1 - postcss-custom-selectors@6.0.3(postcss@8.5.10): + postcss-dir-pseudo-class@10.0.0(postcss@8.5.10): dependencies: postcss: 8.5.10 - postcss-selector-parser: 6.1.2 - - postcss-dir-pseudo-class@6.0.5(postcss@8.5.10): - dependencies: - postcss: 8.5.10 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.1.1 postcss-discard-comments@5.1.2(postcss@8.5.10): dependencies: @@ -10842,14 +11955,10 @@ snapshots: dependencies: postcss: 8.5.10 - postcss-double-position-gradients@3.1.2(postcss@8.5.10): - dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.10) - postcss: 8.5.10 - postcss-value-parser: 4.2.0 - - postcss-env-function@4.0.6(postcss@8.5.10): + postcss-double-position-gradients@7.0.0(postcss@8.5.10): dependencies: + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10) + '@csstools/utilities': 3.0.0(postcss@8.5.10) postcss: 8.5.10 postcss-value-parser: 4.2.0 @@ -10857,53 +11966,53 @@ snapshots: dependencies: postcss: 8.5.10 - postcss-focus-visible@6.0.4(postcss@8.5.10): + postcss-focus-visible@11.0.0(postcss@8.5.10): dependencies: postcss: 8.5.10 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.1.1 - postcss-focus-within@5.0.4(postcss@8.5.10): + postcss-focus-within@10.0.0(postcss@8.5.10): dependencies: postcss: 8.5.10 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.1.1 postcss-font-variant@5.0.0(postcss@8.5.10): dependencies: postcss: 8.5.10 - postcss-gap-properties@3.0.5(postcss@8.5.10): + postcss-gap-properties@7.0.0(postcss@8.5.10): dependencies: postcss: 8.5.10 - postcss-image-set-function@4.0.7(postcss@8.5.10): + postcss-image-set-function@8.0.0(postcss@8.5.10): dependencies: + '@csstools/utilities': 3.0.0(postcss@8.5.10) postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-import@15.1.0(postcss@8.5.10): + postcss-import@16.1.1(postcss@8.5.10): dependencies: postcss: 8.5.10 postcss-value-parser: 4.2.0 read-cache: 1.0.0 - resolve: 1.22.10 + resolve: 1.22.12 - postcss-initial@4.0.1(postcss@8.5.10): + postcss-lab-function@8.0.3(postcss@8.5.10): dependencies: + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10) + '@csstools/utilities': 3.0.0(postcss@8.5.10) postcss: 8.5.10 - postcss-lab-function@4.2.1(postcss@8.5.10): - dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.10) - postcss: 8.5.10 - postcss-value-parser: 4.2.0 - - postcss-load-config@3.1.4(postcss@8.5.10)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)): + postcss-load-config@3.1.4(postcss@8.5.10)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)): dependencies: lilconfig: 2.1.0 yaml: 1.10.2 optionalDependencies: postcss: 8.5.10 - ts-node: 10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3) + ts-node: 10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3) postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(yaml@2.8.1): dependencies: @@ -10914,13 +12023,10 @@ snapshots: tsx: 4.21.0 yaml: 2.8.1 - postcss-logical@5.0.4(postcss@8.5.10): - dependencies: - postcss: 8.5.10 - - postcss-media-minmax@5.0.0(postcss@8.5.10): + postcss-logical@9.0.0(postcss@8.5.10): dependencies: postcss: 8.5.10 + postcss-value-parser: 4.2.0 postcss-merge-longhand@5.1.7(postcss@8.5.10): dependencies: @@ -10993,11 +12099,12 @@ snapshots: postcss-modules-values: 4.0.0(postcss@8.5.10) string-hash: 1.1.3 - postcss-nesting@10.2.0(postcss@8.5.10): + postcss-nesting@14.0.0(postcss@8.5.10): dependencies: - '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.2) + '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1) + '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1) postcss: 8.5.10 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.1.1 postcss-normalize-charset@5.1.0(postcss@8.5.10): dependencies: @@ -11045,7 +12152,7 @@ snapshots: postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-opacity-percentage@1.1.3(postcss@8.5.10): + postcss-opacity-percentage@3.0.0(postcss@8.5.10): dependencies: postcss: 8.5.10 @@ -11055,7 +12162,7 @@ snapshots: postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-overflow-shorthand@3.0.4(postcss@8.5.10): + postcss-overflow-shorthand@7.0.0(postcss@8.5.10): dependencies: postcss: 8.5.10 postcss-value-parser: 4.2.0 @@ -11064,68 +12171,92 @@ snapshots: dependencies: postcss: 8.5.10 - postcss-place@7.0.5(postcss@8.5.10): + postcss-place@11.0.0(postcss@8.5.10): dependencies: postcss: 8.5.10 postcss-value-parser: 4.2.0 - postcss-preset-env@7.8.3(postcss@8.5.10): + postcss-preset-env@11.2.1(postcss@8.5.10): dependencies: - '@csstools/postcss-cascade-layers': 1.1.1(postcss@8.5.10) - '@csstools/postcss-color-function': 1.1.1(postcss@8.5.10) - '@csstools/postcss-font-format-keywords': 1.0.1(postcss@8.5.10) - '@csstools/postcss-hwb-function': 1.0.2(postcss@8.5.10) - '@csstools/postcss-ic-unit': 1.0.1(postcss@8.5.10) - '@csstools/postcss-is-pseudo-class': 2.0.7(postcss@8.5.10) - '@csstools/postcss-nested-calc': 1.0.0(postcss@8.5.10) - '@csstools/postcss-normalize-display-values': 1.0.1(postcss@8.5.10) - '@csstools/postcss-oklab-function': 1.1.1(postcss@8.5.10) - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.10) - '@csstools/postcss-stepped-value-functions': 1.0.1(postcss@8.5.10) - '@csstools/postcss-text-decoration-shorthand': 1.0.0(postcss@8.5.10) - '@csstools/postcss-trigonometric-functions': 1.0.2(postcss@8.5.10) - '@csstools/postcss-unset-value': 1.0.2(postcss@8.5.10) - autoprefixer: 10.4.21(postcss@8.5.10) - browserslist: 4.25.2 - css-blank-pseudo: 3.0.3(postcss@8.5.10) - css-has-pseudo: 3.0.4(postcss@8.5.10) - css-prefers-color-scheme: 6.0.3(postcss@8.5.10) - cssdb: 7.11.2 + '@csstools/postcss-alpha-function': 2.0.4(postcss@8.5.10) + '@csstools/postcss-cascade-layers': 6.0.0(postcss@8.5.10) + '@csstools/postcss-color-function': 5.0.3(postcss@8.5.10) + '@csstools/postcss-color-function-display-p3-linear': 2.0.3(postcss@8.5.10) + '@csstools/postcss-color-mix-function': 4.0.3(postcss@8.5.10) + '@csstools/postcss-color-mix-variadic-function-arguments': 2.0.3(postcss@8.5.10) + '@csstools/postcss-content-alt-text': 3.0.0(postcss@8.5.10) + '@csstools/postcss-contrast-color-function': 3.0.3(postcss@8.5.10) + '@csstools/postcss-exponential-functions': 3.0.2(postcss@8.5.10) + '@csstools/postcss-font-format-keywords': 5.0.0(postcss@8.5.10) + '@csstools/postcss-font-width-property': 1.0.0(postcss@8.5.10) + '@csstools/postcss-gamut-mapping': 3.0.3(postcss@8.5.10) + '@csstools/postcss-gradients-interpolation-method': 6.0.3(postcss@8.5.10) + '@csstools/postcss-hwb-function': 5.0.3(postcss@8.5.10) + '@csstools/postcss-ic-unit': 5.0.0(postcss@8.5.10) + '@csstools/postcss-initial': 3.0.0(postcss@8.5.10) + '@csstools/postcss-is-pseudo-class': 6.0.0(postcss@8.5.10) + '@csstools/postcss-light-dark-function': 3.0.0(postcss@8.5.10) + '@csstools/postcss-logical-float-and-clear': 4.0.0(postcss@8.5.10) + '@csstools/postcss-logical-overflow': 3.0.0(postcss@8.5.10) + '@csstools/postcss-logical-overscroll-behavior': 3.0.0(postcss@8.5.10) + '@csstools/postcss-logical-resize': 4.0.0(postcss@8.5.10) + '@csstools/postcss-logical-viewport-units': 4.0.0(postcss@8.5.10) + '@csstools/postcss-media-minmax': 3.0.2(postcss@8.5.10) + '@csstools/postcss-media-queries-aspect-ratio-number-values': 4.0.0(postcss@8.5.10) + '@csstools/postcss-mixins': 1.0.0(postcss@8.5.10) + '@csstools/postcss-nested-calc': 5.0.0(postcss@8.5.10) + '@csstools/postcss-normalize-display-values': 5.0.1(postcss@8.5.10) + '@csstools/postcss-oklab-function': 5.0.3(postcss@8.5.10) + '@csstools/postcss-position-area-property': 2.0.0(postcss@8.5.10) + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10) + '@csstools/postcss-property-rule-prelude-list': 2.0.0(postcss@8.5.10) + '@csstools/postcss-random-function': 3.0.2(postcss@8.5.10) + '@csstools/postcss-relative-color-syntax': 4.0.3(postcss@8.5.10) + '@csstools/postcss-scope-pseudo-class': 5.0.0(postcss@8.5.10) + '@csstools/postcss-sign-functions': 2.0.2(postcss@8.5.10) + '@csstools/postcss-stepped-value-functions': 5.0.2(postcss@8.5.10) + '@csstools/postcss-syntax-descriptor-syntax-production': 2.0.0(postcss@8.5.10) + '@csstools/postcss-system-ui-font-family': 2.0.0(postcss@8.5.10) + '@csstools/postcss-text-decoration-shorthand': 5.0.3(postcss@8.5.10) + '@csstools/postcss-trigonometric-functions': 5.0.2(postcss@8.5.10) + '@csstools/postcss-unset-value': 5.0.0(postcss@8.5.10) + autoprefixer: 10.5.0(postcss@8.5.10) + browserslist: 4.28.2 + css-blank-pseudo: 8.0.1(postcss@8.5.10) + css-has-pseudo: 8.0.0(postcss@8.5.10) + css-prefers-color-scheme: 11.0.0(postcss@8.5.10) + cssdb: 8.8.0 postcss: 8.5.10 - postcss-attribute-case-insensitive: 5.0.2(postcss@8.5.10) + postcss-attribute-case-insensitive: 8.0.0(postcss@8.5.10) postcss-clamp: 4.1.0(postcss@8.5.10) - postcss-color-functional-notation: 4.2.4(postcss@8.5.10) - postcss-color-hex-alpha: 8.0.4(postcss@8.5.10) - postcss-color-rebeccapurple: 7.1.1(postcss@8.5.10) - postcss-custom-media: 8.0.2(postcss@8.5.10) - postcss-custom-properties: 12.1.11(postcss@8.5.10) - postcss-custom-selectors: 6.0.3(postcss@8.5.10) - postcss-dir-pseudo-class: 6.0.5(postcss@8.5.10) - postcss-double-position-gradients: 3.1.2(postcss@8.5.10) - postcss-env-function: 4.0.6(postcss@8.5.10) - postcss-focus-visible: 6.0.4(postcss@8.5.10) - postcss-focus-within: 5.0.4(postcss@8.5.10) + postcss-color-functional-notation: 8.0.3(postcss@8.5.10) + postcss-color-hex-alpha: 11.0.0(postcss@8.5.10) + postcss-color-rebeccapurple: 11.0.0(postcss@8.5.10) + postcss-custom-media: 12.0.1(postcss@8.5.10) + postcss-custom-properties: 15.0.1(postcss@8.5.10) + postcss-custom-selectors: 9.0.1(postcss@8.5.10) + postcss-dir-pseudo-class: 10.0.0(postcss@8.5.10) + postcss-double-position-gradients: 7.0.0(postcss@8.5.10) + postcss-focus-visible: 11.0.0(postcss@8.5.10) + postcss-focus-within: 10.0.0(postcss@8.5.10) postcss-font-variant: 5.0.0(postcss@8.5.10) - postcss-gap-properties: 3.0.5(postcss@8.5.10) - postcss-image-set-function: 4.0.7(postcss@8.5.10) - postcss-initial: 4.0.1(postcss@8.5.10) - postcss-lab-function: 4.2.1(postcss@8.5.10) - postcss-logical: 5.0.4(postcss@8.5.10) - postcss-media-minmax: 5.0.0(postcss@8.5.10) - postcss-nesting: 10.2.0(postcss@8.5.10) - postcss-opacity-percentage: 1.1.3(postcss@8.5.10) - postcss-overflow-shorthand: 3.0.4(postcss@8.5.10) + postcss-gap-properties: 7.0.0(postcss@8.5.10) + postcss-image-set-function: 8.0.0(postcss@8.5.10) + postcss-lab-function: 8.0.3(postcss@8.5.10) + postcss-logical: 9.0.0(postcss@8.5.10) + postcss-nesting: 14.0.0(postcss@8.5.10) + postcss-opacity-percentage: 3.0.0(postcss@8.5.10) + postcss-overflow-shorthand: 7.0.0(postcss@8.5.10) postcss-page-break: 3.0.4(postcss@8.5.10) - postcss-place: 7.0.5(postcss@8.5.10) - postcss-pseudo-class-any-link: 7.1.6(postcss@8.5.10) + postcss-place: 11.0.0(postcss@8.5.10) + postcss-pseudo-class-any-link: 11.0.0(postcss@8.5.10) postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.10) - postcss-selector-not: 6.0.1(postcss@8.5.10) - postcss-value-parser: 4.2.0 + postcss-selector-not: 9.0.0(postcss@8.5.10) - postcss-pseudo-class-any-link@7.1.6(postcss@8.5.10): + postcss-pseudo-class-any-link@11.0.0(postcss@8.5.10): dependencies: postcss: 8.5.10 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.1.1 postcss-reduce-initial@5.1.2(postcss@8.5.10): dependencies: @@ -11142,10 +12273,10 @@ snapshots: dependencies: postcss: 8.5.10 - postcss-selector-not@6.0.1(postcss@8.5.10): + postcss-selector-not@9.0.0(postcss@8.5.10): dependencies: postcss: 8.5.10 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.1.1 postcss-selector-parser@6.1.2: dependencies: @@ -11157,6 +12288,11 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-svgo@5.1.0(postcss@8.5.10): dependencies: postcss: 8.5.10 @@ -11202,28 +12338,28 @@ snapshots: pretty-bytes@5.6.0: {} - pretty-format@29.7.0: - dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.3.1 - pretty-format@30.0.5: dependencies: '@jest/schemas': 30.0.5 ansi-styles: 5.2.0 react-is: 18.3.1 - prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3): + pretty-format@30.3.0: + dependencies: + '@jest/schemas': 30.0.5 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3): dependencies: '@prisma/config': 7.7.0 - '@prisma/dev': 0.24.3(typescript@5.9.3) + '@prisma/dev': 0.24.3(typescript@6.0.3) '@prisma/engines': 7.7.0 '@prisma/studio-core': 0.27.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) mysql2: 3.15.3 postgres: 3.4.7 optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - '@types/react' - '@types/react-dom' @@ -11256,7 +12392,7 @@ snapshots: proxy-from-env@1.0.0: {} - pump@3.0.3: + pump@3.0.4: dependencies: end-of-stream: 1.4.5 once: 1.4.0 @@ -11265,7 +12401,9 @@ snapshots: pure-rand@7.0.1: {} - qs@6.14.0: + pure-rand@8.4.0: {} + + qs@6.14.2: dependencies: side-channel: 1.1.0 @@ -11273,10 +12411,6 @@ snapshots: raf-schd@4.0.3: {} - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - rc9@2.1.2: dependencies: defu: 6.1.7 @@ -11312,9 +12446,8 @@ snapshots: react: 19.2.5 scheduler: 0.27.0 - react-error-boundary@4.1.2(react@19.2.5): + react-error-boundary@6.1.1(react@19.2.5): dependencies: - '@babel/runtime': 7.28.3 react: 19.2.5 react-hook-form@7.71.1(react@19.2.5): @@ -11329,7 +12462,7 @@ snapshots: dependencies: '@types/use-sync-external-store': 0.0.6 react: 19.2.5 - use-sync-external-store: 1.5.0(react@19.2.5) + use-sync-external-store: 1.6.0(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 redux: 5.0.1 @@ -11349,6 +12482,16 @@ snapshots: react-dom: 19.2.5(react@19.2.5) topojson-client: 3.1.0 + react-simple-maps@3.0.0(prop-types@15.8.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + d3-geo: 2.0.2 + d3-selection: 2.0.0 + d3-zoom: 2.0.0 + prop-types: 15.8.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + topojson-client: 3.1.0 + react-stately@3.46.0(react@19.2.5): dependencies: '@internationalized/date': 3.12.1 @@ -11372,6 +12515,11 @@ snapshots: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) + react-window@2.2.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react@19.2.5: {} read-cache@1.0.0: @@ -11396,14 +12544,16 @@ snapshots: readdirp@4.1.2: {} - redis@4.7.1: + redis@5.12.1: dependencies: - '@redis/bloom': 1.2.0(@redis/client@1.6.1) - '@redis/client': 1.6.1 - '@redis/graph': 1.1.1(@redis/client@1.6.1) - '@redis/json': 1.0.7(@redis/client@1.6.1) - '@redis/search': 1.2.0(@redis/client@1.6.1) - '@redis/time-series': 1.1.0(@redis/client@1.6.1) + '@redis/bloom': 5.12.1(@redis/client@5.12.1) + '@redis/client': 5.12.1 + '@redis/json': 5.12.1(@redis/client@5.12.1) + '@redis/search': 5.12.1(@redis/client@5.12.1) + '@redis/time-series': 5.12.1(@redis/client@5.12.1) + transitivePeerDependencies: + - '@node-rs/xxhash' + - '@opentelemetry/api' redux@5.0.1: {} @@ -11449,14 +12599,19 @@ snapshots: resolve-pkg-maps@1.0.0: {} - resolve.exports@2.0.3: {} - resolve@1.22.10: dependencies: is-core-module: 2.16.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@3.1.0: dependencies: onetime: 5.1.2 @@ -11468,10 +12623,6 @@ snapshots: rfdc@1.4.1: {} - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - rollup-plugin-copy@3.5.0: dependencies: '@types/fs-extra': 8.1.5 @@ -11485,26 +12636,26 @@ snapshots: del: 8.0.1 rollup: 4.60.1 - rollup-plugin-dts@6.4.1(rollup@4.60.1)(typescript@5.9.3): + rollup-plugin-dts@6.4.1(rollup@4.60.1)(typescript@6.0.3): dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 convert-source-map: 2.0.0 magic-string: 0.30.21 rollup: 4.60.1 - typescript: 5.9.3 + typescript: 6.0.3 optionalDependencies: '@babel/code-frame': 7.29.0 - rollup-plugin-node-externals@8.1.2(rollup@4.60.1): - dependencies: + rollup-plugin-node-externals@9.0.1(rollup@4.60.1): + optionalDependencies: rollup: 4.60.1 rollup-plugin-peer-deps-external@2.2.4(rollup@4.60.1): dependencies: rollup: 4.60.1 - rollup-plugin-postcss@4.0.2(postcss@8.5.10)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)): + rollup-plugin-postcss@4.0.2(postcss@8.5.10)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)): dependencies: chalk: 4.1.2 concat-with-sourcemaps: 1.1.0 @@ -11513,7 +12664,7 @@ snapshots: p-queue: 6.6.2 pify: 5.0.0 postcss: 8.5.10 - postcss-load-config: 3.1.4(postcss@8.5.10)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) + postcss-load-config: 3.1.4(postcss@8.5.10)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)) postcss-modules: 4.3.1(postcss@8.5.10) promise.series: 0.2.0 resolve: 1.22.10 @@ -11629,9 +12780,12 @@ snapshots: dependencies: type-fest: 4.41.0 - serialize-javascript@6.0.2: + serialize-error@13.0.1: dependencies: - randombytes: 2.1.0 + non-error: 0.1.0 + type-fest: 5.6.0 + + serialize-javascript@7.0.5: {} set-function-length@1.2.2: dependencies: @@ -11753,7 +12907,7 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 - smob@1.5.0: {} + smob@1.6.1: {} snake-case@3.0.4: dependencies: @@ -11838,7 +12992,7 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 string.prototype.padend@3.1.6: dependencies: @@ -11878,7 +13032,7 @@ snapshots: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.2: + strip-ansi@7.2.0: dependencies: ansi-regex: 6.2.2 @@ -11892,12 +13046,12 @@ snapshots: style-inject@0.3.0: {} - styled-jsx@5.1.6(@babel/core@7.28.3)(react@19.2.5): + styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.5): dependencies: client-only: 0.0.1 react: 19.2.5 optionalDependencies: - '@babel/core': 7.28.3 + '@babel/core': 7.29.0 stylehacks@5.1.1(postcss@8.5.10): dependencies: @@ -11951,6 +13105,14 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 + synckit@0.11.12: + dependencies: + '@pkgr/core': 0.2.9 + + systeminformation@5.31.5: {} + + tagged-tag@1.0.0: {} + tailwind-merge@3.4.0: {} tailwind-variants@3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.18): @@ -11969,18 +13131,18 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - terser@5.43.1: + terser@5.46.1: dependencies: '@jridgewell/source-map': 0.3.11 - acorn: 8.15.0 + acorn: 8.16.0 commander: 2.20.3 source-map-support: 0.5.21 test-exclude@6.0.0: dependencies: - '@istanbuljs/schema': 0.1.3 + '@istanbuljs/schema': 0.1.6 glob: 7.2.3 - minimatch: 3.1.2 + minimatch: 3.1.5 thenby@1.3.4: {} @@ -12008,8 +13170,13 @@ snapshots: tinyglobby@0.2.14: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 tldts-core@6.1.86: {} @@ -12037,55 +13204,57 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.9(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.27.7)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.9(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.29.0))(esbuild@0.28.0)(jest-util@30.3.0)(jest@30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)))(typescript@6.0.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.9 - jest: 29.7.0(@types/node@24.12.2)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3)) + jest: 30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.7.4 type-fest: 4.41.0 - typescript: 5.9.3 + typescript: 6.0.3 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.28.3 - '@jest/transform': 29.7.0 - '@jest/types': 30.0.5 - babel-jest: 29.7.0(@babel/core@7.28.3) - esbuild: 0.27.7 - jest-util: 30.0.5 + '@babel/core': 7.29.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + babel-jest: 30.3.0(@babel/core@7.29.0) + esbuild: 0.28.0 + jest-util: 30.3.0 - ts-morph@27.0.2: + ts-morph@28.0.0: dependencies: - '@ts-morph/common': 0.28.1 + '@ts-morph/common': 0.29.0 code-block-writer: 13.0.3 - ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.21))(@types/node@24.12.2)(typescript@5.9.3): + ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 24.12.2 + '@types/node': 25.6.0 acorn: 8.15.0 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.9.3 + typescript: 6.0.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optionalDependencies: - '@swc/core': 1.15.11(@swc/helpers@0.5.21) + '@swc/core': 1.15.26(@swc/helpers@0.5.21) + + tslib@1.14.1: {} tslib@2.8.1: {} - tsup@8.5.1(@swc/core@1.15.11(@swc/helpers@0.5.21))(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1): + tsup@8.5.1(@swc/core@1.15.26(@swc/helpers@0.5.21))(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.1): dependencies: bundle-require: 5.1.0(esbuild@0.27.7) cac: 6.7.14 @@ -12105,9 +13274,9 @@ snapshots: tinyglobby: 0.2.14 tree-kill: 1.2.2 optionalDependencies: - '@swc/core': 1.15.11(@swc/helpers@0.5.21) + '@swc/core': 1.15.26(@swc/helpers@0.5.21) postcss: 8.5.10 - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - jiti - supports-color @@ -12131,8 +13300,14 @@ snapshots: type-fest@0.21.3: {} + type-fest@0.8.1: {} + type-fest@4.41.0: {} + type-fest@5.6.0: + dependencies: + tagged-tag: 1.0.0 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -12166,7 +13341,7 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript@5.9.3: {} + typescript@6.0.3: {} ua-is-frozen@0.1.2: {} @@ -12188,7 +13363,7 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@7.16.0: {} + undici-types@7.19.2: {} unicorn-magic@0.3.0: {} @@ -12196,6 +13371,30 @@ snapshots: universalify@2.0.1: {} + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + untildify@4.0.0: {} update-browserslist-db@1.1.3(browserslist@4.25.2): @@ -12204,20 +13403,18 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - use-intl@4.8.4(react@19.2.5): + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: - '@formatjs/fast-memoize': 3.1.1 + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-intl@4.9.1(react@19.2.5): + dependencies: + '@formatjs/fast-memoize': 3.1.2 '@schummar/icu-type-parser': 1.21.5 - icu-minify: 4.8.4 - intl-messageformat: 11.2.0 - react: 19.2.5 - - use-memo-one@1.1.3(react@19.2.5): - dependencies: - react: 19.2.5 - - use-sync-external-store@1.5.0(react@19.2.5): - dependencies: + icu-minify: 4.9.1 + intl-messageformat: 11.2.1 react: 19.2.5 use-sync-external-store@1.6.0(react@19.2.5): @@ -12236,13 +13433,13 @@ snapshots: v8-to-istanbul@9.3.0: dependencies: - '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/trace-mapping': 0.3.31 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 - valibot@1.2.0(typescript@5.9.3): + valibot@1.2.0(typescript@6.0.3): optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 validate-npm-package-license@3.0.4: dependencies: @@ -12330,14 +13527,14 @@ snapshots: dependencies: ansi-styles: 6.2.3 string-width: 5.1.2 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 wrappy@1.0.2: {} - write-file-atomic@4.0.2: + write-file-atomic@5.0.1: dependencies: imurmurhash: 0.1.4 - signal-exit: 3.0.7 + signal-exit: 4.1.0 xtend@4.0.2: {} @@ -12345,8 +13542,6 @@ snapshots: yallist@3.1.1: {} - yallist@4.0.0: {} - yallist@5.0.0: {} yaml@1.10.2: {} From df29c16c00e1b8352b0b8832a84d398de8b19591 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Sat, 18 Apr 2026 10:30:31 -0700 Subject: [PATCH 07/59] fix dropdown maxheight --- src/app/(main)/settings/preferences/LanguageSetting.tsx | 2 +- src/app/(main)/settings/preferences/TimezoneSetting.tsx | 2 +- .../(main)/websites/[websiteId]/compare/CompareTables.tsx | 5 ++--- .../(main)/websites/[websiteId]/events/EventProperties.tsx | 2 ++ .../websites/[websiteId]/sessions/SessionProperties.tsx | 1 + src/components/input/BoardSelect.tsx | 2 +- src/components/input/CurrencySelect.tsx | 2 +- src/components/input/LinkSelect.tsx | 2 +- src/components/input/PixelSelect.tsx | 2 +- src/components/input/UserSelect.tsx | 2 +- src/components/input/WebsiteSelect.tsx | 2 +- 11 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/app/(main)/settings/preferences/LanguageSetting.tsx b/src/app/(main)/settings/preferences/LanguageSetting.tsx index 34470c36c..51c3940ae 100644 --- a/src/app/(main)/settings/preferences/LanguageSetting.tsx +++ b/src/app/(main)/settings/preferences/LanguageSetting.tsx @@ -33,7 +33,7 @@ export function LanguageSetting() { allowSearch onSearch={setSearch} onOpenChange={handleOpen} - listProps={{ style: { maxHeight: 300 } }} + maxHeight={480} style={{ minWidth: '250px' }} > {items.map(item => ( diff --git a/src/app/(main)/settings/preferences/TimezoneSetting.tsx b/src/app/(main)/settings/preferences/TimezoneSetting.tsx index f206a6e4f..765d37608 100644 --- a/src/app/(main)/settings/preferences/TimezoneSetting.tsx +++ b/src/app/(main)/settings/preferences/TimezoneSetting.tsx @@ -29,7 +29,7 @@ export function TimezoneSetting() { allowSearch={true} onSearch={setSearch} onOpenChange={handleOpen} - listProps={{ style: { maxHeight: 300 } }} + maxHeight={480} style={{ minWidth: '250px' }} > {items.map((item: any) => ( diff --git a/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx b/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx index 3a07f9e66..683dcd493 100644 --- a/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx +++ b/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx @@ -154,10 +154,9 @@ export function CompareTables({ websiteId }: { websiteId: string }) { - {events?.map(p => ( - - {p} - - ))} - - + + + + + + + + {properties.map((field: { propertyName: string }) => ( + + {field.propertyName} + + ))} + + )} {eventName && propertyName && ( - + + {selectedProperty?.dataType === 1 && ( + + )} + {selectedProperty?.dataType === 2 && ( + + )} + )} + {eventName && } ); } - -const EventValues = ({ websiteId, eventName, propertyName }) => { - const { - data: values, - isLoading, - isFetching, - error, - } = useEventDataValuesQuery(websiteId, eventName, propertyName); - - const propertySum = useMemo(() => { - return values?.reduce((sum, { total }) => sum + total, 0) ?? 0; - }, [values]); - - const chartData = useMemo(() => { - if (!propertyName || !values) return null; - return { - labels: values.map(({ value }) => value), - datasets: [ - { - data: values.map(({ total }) => total), - backgroundColor: CHART_COLORS, - borderWidth: 0, - }, - ], - }; - }, [propertyName, values]); - - const tableData = useMemo(() => { - if (!propertyName || !values || propertySum === 0) return []; - return values.map(({ value, total }) => ({ - label: value, - count: total, - percent: 100 * (total / propertySum), - })); - }, [propertyName, values, propertySum]); - - return ( - - {values && ( - - - - - )} - - ); -}; diff --git a/src/app/api/websites/[websiteId]/event-data-pivot/numeric-series/route.ts b/src/app/api/websites/[websiteId]/event-data-pivot/numeric-series/route.ts new file mode 100644 index 000000000..37e1047f4 --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data-pivot/numeric-series/route.ts @@ -0,0 +1,38 @@ +import { z } from 'zod'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getEventDataNumericSeries } from '@/queries/sql/events/getEventDataNumericSeries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + eventName: z.string(), + propertyName: z.string(), + metric: z.enum(['sum', 'avg']).default('sum'), + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { eventName, propertyName, metric, ...rest } = query; + const filters = await getQueryFilters(rest, websiteId); + const data = await getEventDataNumericSeries(websiteId, eventName, propertyName, metric, filters); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/event-data-pivot/property-series/route.ts b/src/app/api/websites/[websiteId]/event-data-pivot/property-series/route.ts new file mode 100644 index 000000000..7e923689f --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data-pivot/property-series/route.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getEventDataPropertySeries } from '@/queries/sql/events/getEventDataPropertySeries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + eventName: z.string(), + propertyName: z.string(), + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { eventName, propertyName, ...rest } = query; + const filters = await getQueryFilters(rest, websiteId); + const data = await getEventDataPropertySeries(websiteId, eventName, propertyName, filters); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/event-data-pivot/route.ts b/src/app/api/websites/[websiteId]/event-data-pivot/route.ts new file mode 100644 index 000000000..37216f6e3 --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data-pivot/route.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams, pagingParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getEventDataPivot } from '@/queries/sql/events/getEventDataPivot'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + eventName: z.string(), + ...filterParams, + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { eventName, ...rest } = query; + const filters = await getQueryFilters(rest, websiteId); + const result = await getEventDataPivot(websiteId, eventName, filters); + + return json(result); +} diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts index 1b27b684a..cfc71afb8 100644 --- a/src/components/hooks/index.ts +++ b/src/components/hooks/index.ts @@ -18,7 +18,11 @@ export * from './queries/useDashboardQuery'; export * from './queries/useDateRangeQuery'; export * from './queries/useDeleteQuery'; export * from './queries/useEventDataEventsQuery'; +export * from './queries/useEventDataFieldsQuery'; +export * from './queries/useEventDataNumericSeriesQuery'; +export * from './queries/useEventDataPivotQuery'; export * from './queries/useEventDataPropertiesQuery'; +export * from './queries/useEventDataPropertySeriesQuery'; export * from './queries/useEventDataQuery'; export * from './queries/useEventDataValuesQuery'; export * from './queries/useLinkQuery'; diff --git a/src/components/hooks/queries/useEventDataFieldsQuery.ts b/src/components/hooks/queries/useEventDataFieldsQuery.ts new file mode 100644 index 000000000..bd9e0f5ce --- /dev/null +++ b/src/components/hooks/queries/useEventDataFieldsQuery.ts @@ -0,0 +1,32 @@ +import type { ReactQueryOptions } from '@/lib/types'; +import { useApi } from '../useApi'; +import { useDateParameters } from '../useDateParameters'; +import { useFilterParameters } from '../useFilterParameters'; + +export function useEventDataFieldsQuery( + websiteId: string, + eventName?: string, + options?: ReactQueryOptions, +) { + const { get, useQuery } = useApi(); + const { startAt, endAt, unit, timezone } = useDateParameters(); + const params = useFilterParameters(); + + return useQuery({ + queryKey: [ + 'websites:event-data:fields', + { websiteId, eventName, startAt, endAt, unit, timezone, ...params }, + ], + queryFn: () => + get(`/websites/${websiteId}/event-data/fields`, { + event: eventName, + startAt, + endAt, + unit, + timezone, + ...params, + }), + enabled: !!websiteId, + ...options, + }); +} diff --git a/src/components/hooks/queries/useEventDataNumericSeriesQuery.ts b/src/components/hooks/queries/useEventDataNumericSeriesQuery.ts new file mode 100644 index 000000000..eb8b63638 --- /dev/null +++ b/src/components/hooks/queries/useEventDataNumericSeriesQuery.ts @@ -0,0 +1,36 @@ +import type { ReactQueryOptions } from '@/lib/types'; +import { useApi } from '../useApi'; +import { useDateParameters } from '../useDateParameters'; +import { useFilterParameters } from '../useFilterParameters'; + +export function useEventDataNumericSeriesQuery( + websiteId: string, + eventName: string, + propertyName: string, + metric: 'sum' | 'avg', + options?: ReactQueryOptions, +) { + const { get, useQuery } = useApi(); + const { startAt, endAt, unit, timezone } = useDateParameters(); + const params = useFilterParameters(); + + return useQuery({ + queryKey: [ + 'websites:event-data-pivot:numeric-series', + { websiteId, eventName, propertyName, metric, startAt, endAt, unit, timezone, ...params }, + ], + queryFn: () => + get(`/websites/${websiteId}/event-data-pivot/numeric-series`, { + eventName, + propertyName, + metric, + startAt, + endAt, + unit, + timezone, + ...params, + }), + enabled: !!(websiteId && eventName && propertyName), + ...options, + }); +} diff --git a/src/components/hooks/queries/useEventDataPivotQuery.ts b/src/components/hooks/queries/useEventDataPivotQuery.ts new file mode 100644 index 000000000..e805c2b91 --- /dev/null +++ b/src/components/hooks/queries/useEventDataPivotQuery.ts @@ -0,0 +1,32 @@ +import type { ReactQueryOptions } from '@/lib/types'; +import { useApi } from '../useApi'; +import { useDateParameters } from '../useDateParameters'; +import { useFilterParameters } from '../useFilterParameters'; + +export function useEventDataPivotQuery( + websiteId: string, + eventName: string, + options?: ReactQueryOptions, +) { + const { get, useQuery } = useApi(); + const { startAt, endAt, unit, timezone } = useDateParameters(); + const params = useFilterParameters(); + + return useQuery({ + queryKey: [ + 'websites:event-data-pivot', + { websiteId, eventName, startAt, endAt, unit, timezone, ...params }, + ], + queryFn: () => + get(`/websites/${websiteId}/event-data-pivot`, { + eventName, + startAt, + endAt, + unit, + timezone, + ...params, + }), + enabled: !!(websiteId && eventName), + ...options, + }); +} diff --git a/src/components/hooks/queries/useEventDataPropertySeriesQuery.ts b/src/components/hooks/queries/useEventDataPropertySeriesQuery.ts new file mode 100644 index 000000000..a60f80442 --- /dev/null +++ b/src/components/hooks/queries/useEventDataPropertySeriesQuery.ts @@ -0,0 +1,34 @@ +import type { ReactQueryOptions } from '@/lib/types'; +import { useApi } from '../useApi'; +import { useDateParameters } from '../useDateParameters'; +import { useFilterParameters } from '../useFilterParameters'; + +export function useEventDataPropertySeriesQuery( + websiteId: string, + eventName: string, + propertyName: string, + options?: ReactQueryOptions, +) { + const { get, useQuery } = useApi(); + const { startAt, endAt, unit, timezone } = useDateParameters(); + const params = useFilterParameters(); + + return useQuery({ + queryKey: [ + 'websites:event-data-pivot:property-series', + { websiteId, eventName, propertyName, startAt, endAt, unit, timezone, ...params }, + ], + queryFn: () => + get(`/websites/${websiteId}/event-data-pivot/property-series`, { + eventName, + propertyName, + startAt, + endAt, + unit, + timezone, + ...params, + }), + enabled: !!(websiteId && eventName && propertyName), + ...options, + }); +} diff --git a/src/components/messages.ts b/src/components/messages.ts index 5046f1904..d5d77c206 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -134,6 +134,7 @@ export const labels: Record = { thisYear: 'label.this-year', allTime: 'label.all-time', customRange: 'label.custom-range', + selectEvent: 'label.select-event', selectWebsite: 'label.select-website', selectLink: 'label.select-link', selectPixel: 'label.select-pixel', diff --git a/src/queries/sql/events/getEventDataFields.ts b/src/queries/sql/events/getEventDataFields.ts index 709454929..fe4f7b813 100644 --- a/src/queries/sql/events/getEventDataFields.ts +++ b/src/queries/sql/events/getEventDataFields.ts @@ -13,7 +13,7 @@ export async function getEventDataFields(...args: [websiteId: string, filters: Q } async function relationalQuery(websiteId: string, filters: QueryFilters) { - const { rawQuery, parseFilters, getDateSQL } = prisma; + const { rawQuery, parseFilters } = prisma; const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, websiteId, @@ -24,11 +24,6 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { select data_key as "propertyName", data_type as "dataType", - case - when data_type = 2 then replace(string_value, '.0000', '') - when data_type = 4 then ${getDateSQL('date_value', 'hour')} - else string_value - end as "value", count(*) as "total" from event_data join website_event on website_event.event_id = event_data.website_event_id @@ -39,9 +34,8 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { where event_data.website_id = {{websiteId::uuid}} and event_data.created_at between {{startDate}} and {{endDate}} ${filterQuery} - group by data_key, data_type, value - order by 2 desc - limit 100 + group by data_key, data_type + order by "total" desc, "propertyName" asc `, queryParams, FUNCTION_NAME, @@ -51,7 +45,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { async function clickhouseQuery( websiteId: string, filters: QueryFilters, -): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> { +): Promise<{ propertyName: string; dataType: number; total: number }[]> { const { rawQuery, parseFilters } = clickhouse; const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId }); @@ -60,13 +54,10 @@ async function clickhouseQuery( select data_key as propertyName, data_type as dataType, - multiIf(data_type = 2, replaceAll(string_value, '.0000', ''), - data_type = 4, toString(date_trunc('hour', date_value)), - string_value) as "value", count(*) as "total" from event_data any left join ( - select * + select * from website_event where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} @@ -78,9 +69,8 @@ async function clickhouseQuery( where event_data.website_id = {websiteId:UUID} and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} ${filterQuery} - group by data_key, data_type, value - order by 2 desc - limit 100 + group by data_key, data_type + order by total desc, propertyName asc `, queryParams, FUNCTION_NAME, diff --git a/src/queries/sql/events/getEventDataNumericSeries.ts b/src/queries/sql/events/getEventDataNumericSeries.ts new file mode 100644 index 000000000..5b0ef85d2 --- /dev/null +++ b/src/queries/sql/events/getEventDataNumericSeries.ts @@ -0,0 +1,104 @@ +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import type { QueryFilters } from '@/lib/types'; + +const FUNCTION_NAME = 'getEventDataNumericSeries'; + +export async function getEventDataNumericSeries( + ...args: [ + websiteId: string, + eventName: string, + propertyName: string, + metric: 'sum' | 'avg', + filters: QueryFilters, + ] +) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery( + websiteId: string, + eventName: string, + propertyName: string, + metric: 'sum' | 'avg', + filters: QueryFilters, +) { + const { timezone = 'utc', unit = 'day' } = filters; + const { rawQuery, getDateSQL, parseFilters } = prisma; + const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + }); + const aggFn = metric === 'avg' ? 'avg' : 'sum'; + + return rawQuery( + ` + select + ${getDateSQL('event_data.created_at', unit, timezone)} t, + ${aggFn}(cast(event_data.number_value as decimal)) y + from event_data + join website_event on website_event.event_id = event_data.website_event_id + and website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_type = 2 + and website_event.event_name = {{eventName}} + ${cohortQuery} + ${joinSessionQuery} + where event_data.website_id = {{websiteId::uuid}} + and event_data.created_at between {{startDate}} and {{endDate}} + and event_data.data_key = {{propertyName}} + and event_data.data_type = 2 + ${filterQuery} + group by 1 + order by 1 + `, + { ...queryParams, eventName, propertyName }, + FUNCTION_NAME, + ); +} + +async function clickhouseQuery( + websiteId: string, + eventName: string, + propertyName: string, + metric: 'sum' | 'avg', + filters: QueryFilters, +): Promise<{ t: string; y: number }[]> { + const { timezone = 'UTC', unit = 'day' } = filters; + const { rawQuery, getDateSQL, parseFilters } = clickhouse; + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId }); + const aggFn = metric === 'avg' ? 'avg' : 'sum'; + + return rawQuery( + ` + select + ${getDateSQL('event_data.created_at', unit, timezone)} as t, + ${aggFn}(event_data.number_value) as y + from event_data + any left join ( + select * + from website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_type = 2 + and event_name = {eventName:String}) website_event + on website_event.event_id = event_data.event_id + and website_event.session_id = event_data.session_id + and website_event.website_id = event_data.website_id + ${cohortQuery} + where event_data.website_id = {websiteId:UUID} + and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_data.data_key = {propertyName:String} + and event_data.data_type = 2 + ${filterQuery} + group by t + order by t + `, + { ...queryParams, eventName, propertyName }, + FUNCTION_NAME, + ); +} diff --git a/src/queries/sql/events/getEventDataPivot.ts b/src/queries/sql/events/getEventDataPivot.ts new file mode 100644 index 000000000..eb69fa410 --- /dev/null +++ b/src/queries/sql/events/getEventDataPivot.ts @@ -0,0 +1,173 @@ +import clickhouse from '@/lib/clickhouse'; +import { DEFAULT_PAGE_SIZE } from '@/lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import type { QueryFilters } from '@/lib/types'; + +const FUNCTION_NAME = 'getEventDataPivot'; + +export async function getEventDataPivot( + ...args: [websiteId: string, eventName: string, filters: QueryFilters] +) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery(websiteId: string, eventName: string, filters: QueryFilters) { + const { rawQuery, parseFilters } = prisma; + const { page = 1, pageSize } = filters; + const size = +pageSize || DEFAULT_PAGE_SIZE; + const offset = +size * (+page - 1); + + const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + }); + + const countResult = await rawQuery( + ` + select count(distinct website_event.event_id) as num + from website_event + join event_data on event_data.website_event_id = website_event.event_id + and event_data.website_id = {{websiteId::uuid}} + and event_data.created_at between {{startDate}} and {{endDate}} + ${cohortQuery} + ${joinSessionQuery} + where website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_name = {{eventName}} + ${filterQuery} + `, + { ...queryParams, eventName }, + ); + + const count = countResult[0].num; + + const rows = await rawQuery( + ` + with paged_events as ( + select website_event.event_id + from website_event + join event_data on event_data.website_event_id = website_event.event_id + and event_data.website_id = {{websiteId::uuid}} + and event_data.created_at between {{startDate}} and {{endDate}} + ${cohortQuery} + ${joinSessionQuery} + where website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_name = {{eventName}} + ${filterQuery} + group by website_event.event_id + order by max(website_event.created_at) desc + limit ${size} offset ${offset} + ) + select + website_event.event_id as "eventId", + website_event.session_id as "sessionId", + website_event.event_name as "eventName", + website_event.url_path as "urlPath", + website_event.created_at as "createdAt", + event_data.data_key as "dataKey", + coalesce( + case when event_data.data_type = 1 then event_data.string_value end, + case when event_data.data_type = 2 then cast(event_data.number_value as varchar) end, + case when event_data.data_type = 4 then cast(event_data.date_value as varchar) end, + '' + ) as "value", + event_data.data_type as "dataType" + from event_data + join website_event on website_event.event_id = event_data.website_event_id + and website_event.website_id = {{websiteId::uuid}} + join paged_events on paged_events.event_id = event_data.website_event_id + where event_data.website_id = {{websiteId::uuid}} + and event_data.created_at between {{startDate}} and {{endDate}} + order by website_event.created_at desc + `, + { ...queryParams, eventName }, + FUNCTION_NAME, + ); + + // Pivot flat rows into one record per event + const eventMap = new Map< + string, + { eventId: string; sessionId: string; eventName: string; urlPath: string; createdAt: Date; propertyKeys: string[]; propertyValues: string[] } + >(); + for (const { eventId, sessionId, eventName: name, urlPath, createdAt, dataKey, value } of rows) { + if (!eventMap.has(eventId)) { + eventMap.set(eventId, { eventId, sessionId, eventName: name, urlPath, createdAt, propertyKeys: [], propertyValues: [] }); + } + const entry = eventMap.get(eventId); + entry.propertyKeys.push(dataKey); + entry.propertyValues.push(value ?? ''); + } + + return { data: [...eventMap.values()], count, page: +page, pageSize: size }; +} + +async function clickhouseQuery(websiteId: string, eventName: string, filters: QueryFilters) { + const { rawQuery, parseFilters } = clickhouse; + const { page = 1, pageSize } = filters; + const size = +pageSize || DEFAULT_PAGE_SIZE; + const offset = +size * (+page - 1); + + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId }); + + const count = await rawQuery( + ` + select count() as num + from umami.event_data_pivot + any left join ( + select * + from website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_type = 2 + and event_name = {eventName:String}) website_event + on website_event.event_id = event_data_pivot.event_id + and website_event.session_id = event_data_pivot.session_id + and website_event.website_id = event_data_pivot.website_id + ${cohortQuery} + where event_data_pivot.website_id = {websiteId:UUID} + and event_data_pivot.created_at between {startDate:DateTime64} and {endDate:DateTime64} + ${filterQuery} + `, + { ...queryParams, eventName }, + ).then((res: any) => res[0].num); + + const data = await rawQuery( + ` + select + event_id as eventId, + session_id as sessionId, + event_name as eventName, + url_path as urlPath, + created_at as createdAt, + groupArrayMerge(property_keys) as propertyKeys, + groupArrayMerge(property_values) as propertyValues + from umami.event_data_pivot + any left join ( + select * + from website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_type = 2 + and event_name = {eventName:String}) website_event + on website_event.event_id = event_data_pivot.event_id + and website_event.session_id = event_data_pivot.session_id + and website_event.website_id = event_data_pivot.website_id + ${cohortQuery} + where event_data_pivot.website_id = {websiteId:UUID} + and event_data_pivot.created_at between {startDate:DateTime64} and {endDate:DateTime64} + ${filterQuery} + group by event_id, session_id, event_name, url_path, created_at + order by created_at desc + limit ${size} offset ${offset} + `, + { ...queryParams, eventName }, + FUNCTION_NAME, + ); + + return { data, count, page: +page, pageSize: size }; +} diff --git a/src/queries/sql/events/getEventDataProperties.ts b/src/queries/sql/events/getEventDataProperties.ts index 82d0b6b62..b7b7c7379 100644 --- a/src/queries/sql/events/getEventDataProperties.ts +++ b/src/queries/sql/events/getEventDataProperties.ts @@ -31,6 +31,7 @@ async function relationalQuery( select website_event.event_name as "eventName", event_data.data_key as "propertyName", + event_data.data_type as "dataType", count(*) as "total" from event_data join website_event on website_event.event_id = event_data.website_event_id @@ -41,8 +42,8 @@ async function relationalQuery( where event_data.website_id = {{websiteId::uuid}} and event_data.created_at between {{startDate}} and {{endDate}} ${filterQuery} - group by website_event.event_name, event_data.data_key - order by 3 desc + group by website_event.event_name, event_data.data_key, event_data.data_type + order by 4 desc limit 500 `, queryParams, @@ -53,7 +54,7 @@ async function relationalQuery( async function clickhouseQuery( websiteId: string, filters: QueryFilters & { propertyName?: string }, -): Promise<{ eventName: string; propertyName: string; total: number }[]> { +): Promise<{ eventName: string; propertyName: string; dataType: number; total: number }[]> { const { rawQuery, parseFilters } = clickhouse; const { filterQuery, cohortQuery, queryParams } = parseFilters( { ...filters, websiteId }, @@ -67,6 +68,7 @@ async function clickhouseQuery( select event_name as eventName, data_key as propertyName, + data_type as dataType, count(*) as total from event_data any left join ( @@ -82,8 +84,8 @@ async function clickhouseQuery( where event_data.website_id = {websiteId:UUID} and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} ${filterQuery} - group by event_name, data_key - order by 1, 3 desc + group by event_name, data_key, data_type + order by 1, 4 desc limit 500 `, queryParams, diff --git a/src/queries/sql/events/getEventDataPropertySeries.ts b/src/queries/sql/events/getEventDataPropertySeries.ts new file mode 100644 index 000000000..deb1c65b9 --- /dev/null +++ b/src/queries/sql/events/getEventDataPropertySeries.ts @@ -0,0 +1,96 @@ +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import type { QueryFilters } from '@/lib/types'; + +const FUNCTION_NAME = 'getEventDataPropertySeries'; + +export async function getEventDataPropertySeries( + ...args: [websiteId: string, eventName: string, propertyName: string, filters: QueryFilters] +) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery( + websiteId: string, + eventName: string, + propertyName: string, + filters: QueryFilters, +) { + const { timezone = 'utc', unit = 'day' } = filters; + const { rawQuery, getDateSQL, parseFilters } = prisma; + const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + }); + + return rawQuery( + ` + select + event_data.string_value as x, + ${getDateSQL('event_data.created_at', unit, timezone)} t, + count(*) y + from event_data + join website_event on website_event.event_id = event_data.website_event_id + and website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_type = 2 + and website_event.event_name = {{eventName}} + ${cohortQuery} + ${joinSessionQuery} + where event_data.website_id = {{websiteId::uuid}} + and event_data.created_at between {{startDate}} and {{endDate}} + and event_data.data_key = {{propertyName}} + and event_data.data_type = 1 + ${filterQuery} + group by 1, 2 + order by 2 + `, + { ...queryParams, eventName, propertyName }, + FUNCTION_NAME, + ); +} + +async function clickhouseQuery( + websiteId: string, + eventName: string, + propertyName: string, + filters: QueryFilters, +): Promise<{ x: string; t: string; y: number }[]> { + const { timezone = 'UTC', unit = 'day' } = filters; + const { rawQuery, getDateSQL, parseFilters } = clickhouse; + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId }); + + return rawQuery( + ` + select + event_data.string_value as x, + ${getDateSQL('event_data.created_at', unit, timezone)} as t, + count() as y + from event_data + any left join ( + select * + from website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_type = 2 + and event_name = {eventName:String}) website_event + on website_event.event_id = event_data.event_id + and website_event.session_id = event_data.session_id + and website_event.website_id = event_data.website_id + ${cohortQuery} + where event_data.website_id = {websiteId:UUID} + and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_data.data_key = {propertyName:String} + and event_data.data_type = 1 + ${filterQuery} + group by x, t + order by t + `, + { ...queryParams, eventName, propertyName }, + FUNCTION_NAME, + ); +} diff --git a/src/queries/sql/index.ts b/src/queries/sql/index.ts index 0b164a458..94e283df3 100644 --- a/src/queries/sql/index.ts +++ b/src/queries/sql/index.ts @@ -1,7 +1,10 @@ export * from './events/getEventDataById'; export * from './events/getEventDataEvents'; export * from './events/getEventDataFields'; +export * from './events/getEventDataNumericSeries'; +export * from './events/getEventDataPivot'; export * from './events/getEventDataProperties'; +export * from './events/getEventDataPropertySeries'; export * from './events/getEventDataStats'; export * from './events/getEventDataUsage'; export * from './events/getEventDataValues'; From cf2ab575ba678f36c52e55ebddf5310cc3e904b1 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Thu, 23 Apr 2026 11:24:32 -0700 Subject: [PATCH 44/59] mobile-friendly changes --- .../[websiteId]/event-data/EventDataPivotTable.tsx | 7 ++++--- .../[websiteId]/event-data/EventDataPropertyChart.tsx | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataPivotTable.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataPivotTable.tsx index f2cb20172..a0aad0e39 100644 --- a/src/app/(main)/websites/[websiteId]/event-data/EventDataPivotTable.tsx +++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataPivotTable.tsx @@ -7,7 +7,7 @@ import { Empty } from '@/components/common/Empty'; import Link from '@/components/common/Link'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { Pager } from '@/components/common/Pager'; -import { useEventDataPivotQuery, useEventDataPropertiesQuery, useMessages, useNavigation } from '@/components/hooks'; +import { useEventDataPivotQuery, useEventDataPropertiesQuery, useMessages, useMobile, useNavigation } from '@/components/hooks'; export function EventDataPivotTable({ websiteId, @@ -18,6 +18,7 @@ export function EventDataPivotTable({ }) { const { t, labels } = useMessages(); const { router, updateParams } = useNavigation(); + const { isMobile } = useMobile(); const propertiesQuery = useEventDataPropertiesQuery(websiteId); const pivotQuery = useEventDataPivotQuery(websiteId, eventName); @@ -72,8 +73,8 @@ export function EventDataPivotTable({ renderEmpty={() => } > - - + + {(row: any) => ( diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataPropertyChart.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataPropertyChart.tsx index 940ec7bc3..5a7f8e5d1 100644 --- a/src/app/(main)/websites/[websiteId]/event-data/EventDataPropertyChart.tsx +++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataPropertyChart.tsx @@ -144,7 +144,7 @@ export function EventDataPropertyChart({ error={valuesQuery.error} minHeight="300px" > - + {pieChartData && } From 77dda7268ee3574e42df138e913ef52557f8f92e Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 24 Apr 2026 11:39:40 -0700 Subject: [PATCH 45/59] add event-data property filtering --- public/intl/messages/en-US.json | 1 + .../event-data/EventDataFilterBar.tsx | 96 ++++++++++++++ .../event-data/EventDataFilterButton.tsx | 34 +++++ .../event-data/EventDataFilterEditForm.tsx | 108 ++++++++++++++++ .../event-data/EventDataFilterRecord.tsx | 121 ++++++++++++++++++ .../event-data/EventDataNumericChart.tsx | 5 + .../event-data/EventDataPivotTable.tsx | 5 +- .../event-data/EventDataPropertyChart.tsx | 103 ++++++++------- .../[websiteId]/events/EventProperties.tsx | 96 +++++++++----- .../event-data-pivot/numeric-series/route.ts | 4 +- .../event-data-pivot/property-series/route.ts | 4 +- .../[websiteId]/event-data-pivot/route.ts | 4 +- .../queries/useEventDataNumericSeriesQuery.ts | 7 +- .../hooks/queries/useEventDataPivotQuery.ts | 7 +- .../useEventDataPropertySeriesQuery.ts | 7 +- src/components/input/FilterBar.tsx | 5 +- src/components/messages.ts | 1 + src/lib/clickhouse.ts | 78 ++++++++++- src/lib/params.ts | 30 ++++- src/lib/prisma.ts | 67 +++++++++- src/lib/request.ts | 8 +- src/lib/types.ts | 7 + .../sql/events/getEventDataNumericSeries.ts | 17 ++- src/queries/sql/events/getEventDataPivot.ts | 31 +++-- .../sql/events/getEventDataPropertySeries.ts | 24 +++- 25 files changed, 748 insertions(+), 122 deletions(-) create mode 100644 src/app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx create mode 100644 src/app/(main)/websites/[websiteId]/event-data/EventDataFilterButton.tsx create mode 100644 src/app/(main)/websites/[websiteId]/event-data/EventDataFilterEditForm.tsx create mode 100644 src/app/(main)/websites/[websiteId]/event-data/EventDataFilterRecord.tsx diff --git a/public/intl/messages/en-US.json b/public/intl/messages/en-US.json index 035bc83fd..38a7460d3 100644 --- a/public/intl/messages/en-US.json +++ b/public/intl/messages/en-US.json @@ -125,6 +125,7 @@ "filter-combined": "Combined", "filter-raw": "Raw", "filters": "Filters", + "property-filter": "Property Filter", "first-click": "First click", "first-seen": "First seen", "funnel": "Funnel", diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx new file mode 100644 index 000000000..a424d595a --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx @@ -0,0 +1,96 @@ +'use client'; +import { Button, Icon, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen'; +import { useMessages } from '@/components/hooks'; +import { X } from '@/components/icons'; +import type { EventPropertyFilter } from '@/lib/types'; + +export function EventDataFilterBar({ + filters, + onChange, +}: { + filters: EventPropertyFilter[]; + onChange: (filters: EventPropertyFilter[]) => void; +}) { + const { t, labels } = useMessages(); + + if (!filters.length) return null; + + const operatorLabel = (op: string) => { + switch (op) { + case 'eq': return t(labels.is); + case 'neq': return t(labels.isNot); + case 'c': return t(labels.contains); + case 'dnc': return t(labels.doesNotContain); + case 'regex': return t(labels.regexMatch); + case 'notRegex': return t(labels.regexNotMatch); + case 'gt': return t(labels.greaterThan); + case 'lt': return t(labels.lessThan); + case 'gte': return t(labels.greaterThanEquals); + case 'lte': return t(labels.lessThanEquals); + default: return op; + } + }; + + const handleRemove = (index: number) => { + onChange(filters.filter((_, i) => i !== index)); + }; + + return ( + + + {filters.map((filter, index) => ( + handleRemove(index)} + /> + ))} + + + + + {t(labels.clearAll)} + + + + ); +} + +function FilterPill({ + label, + operator, + value, + onRemove, +}: { + label: string; + operator: string; + value: string; + onRemove: () => void; +}) { + return ( + + + + {label} + + {operator} + + {value} + + + + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterButton.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterButton.tsx new file mode 100644 index 000000000..4f39b41e2 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterButton.tsx @@ -0,0 +1,34 @@ +'use client'; +import { useMessages } from '@/components/hooks'; +import { ListFilter } from '@/components/icons'; +import { DialogButton } from '@/components/input/DialogButton'; +import type { EventPropertyFilter } from '@/lib/types'; +import { EventDataFilterEditForm } from './EventDataFilterEditForm'; + +export function EventDataFilterButton({ + websiteId, + eventName, + eventFilters, + onApply, +}: { + websiteId: string; + eventName: string; + eventFilters: EventPropertyFilter[]; + onApply: (filters: EventPropertyFilter[]) => void; +}) { + const { t, labels } = useMessages(); + + return ( + } label={t(labels.propertyFilter)} variant="outline"> + {({ close }) => ( + + )} + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterEditForm.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterEditForm.tsx new file mode 100644 index 000000000..82030cd32 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterEditForm.tsx @@ -0,0 +1,108 @@ +'use client'; +import { Button, Column, Grid, Icon, List, ListItem, Menu, MenuItem, MenuTrigger, Popover, Row } from '@umami/react-zen'; +import { useState } from 'react'; +import { Empty } from '@/components/common/Empty'; +import { useEventDataFieldsQuery, useMessages, useMobile } from '@/components/hooks'; +import { Plus } from '@/components/icons'; +import type { EventPropertyFilter } from '@/lib/types'; +import { EventDataFilterRecord } from './EventDataFilterRecord'; + +export function EventDataFilterEditForm({ + websiteId, + eventName, + value, + onApply, + onClose, +}: { + websiteId: string; + eventName: string; + value: EventPropertyFilter[]; + onApply: (filters: EventPropertyFilter[]) => void; + onClose: () => void; +}) { + const { t, labels, messages } = useMessages(); + const { isMobile } = useMobile(); + const [filters, setFilters] = useState(value); + + const { data: fields = [] } = useEventDataFieldsQuery(websiteId, eventName); + + const handleAdd = (propertyName: string) => { + const field = (fields as any[]).find(f => f.propertyName === propertyName); + const dataType: number = field?.dataType ?? 1; + setFilters(prev => [...prev, { propertyName, dataType, operator: 'eq', value: '' }]); + }; + + const handleChange = (index: number, filter: EventPropertyFilter) => { + setFilters(prev => prev.map((f, i) => (i === index ? filter : f))); + }; + + const handleRemove = (index: number) => { + setFilters(prev => prev.filter((_, i) => i !== index)); + }; + + return ( + + + + + + + + handleAdd(key.toString())} + style={{ maxHeight: 'calc(100vh - 2rem)', overflowY: 'auto' }} + > + {(fields as any[]).map(field => ( + + {field.propertyName} + + ))} + + + + + + handleAdd(key.toString())}> + {(fields as any[]).map(field => ( + + {field.propertyName} + + ))} + + + + {filters.map((filter, index) => ( + handleChange(index, f)} + onRemove={() => handleRemove(index)} + /> + ))} + {!filters.length && } + + + + + + + + + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterRecord.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterRecord.tsx new file mode 100644 index 000000000..4ca358029 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterRecord.tsx @@ -0,0 +1,121 @@ +'use client'; +import { Button, Column, Grid, Icon, Label, ListItem, Loading, Select, TextField } from '@umami/react-zen'; +import { useState } from 'react'; +import { Empty } from '@/components/common/Empty'; +import { MultiSelect } from '@/components/common/MultiSelect'; +import { useEventDataValuesQuery, useMessages } from '@/components/hooks'; +import { X } from '@/components/icons'; +import type { EventPropertyFilter } from '@/lib/types'; + +const STRING_OPERATORS = ['eq', 'neq', 'c', 'dnc', 'regex', 'notRegex'] as const; +const NUMERIC_OPERATORS = ['eq', 'neq', 'gt', 'lt', 'gte', 'lte'] as const; + +export function EventDataFilterRecord({ + websiteId, + eventName, + filter, + onChange, + onRemove, +}: { + websiteId: string; + eventName: string; + filter: EventPropertyFilter; + onChange: (filter: EventPropertyFilter) => void; + onRemove: () => void; +}) { + const { t, labels, messages } = useMessages(); + const [search, setSearch] = useState(''); + + const isNumeric = filter.dataType === 2; + const operators = isNumeric ? NUMERIC_OPERATORS : STRING_OPERATORS; + const isFreeText = filter.operator === 'c' || filter.operator === 'dnc' || filter.operator === 'regex' || filter.operator === 'notRegex'; + + const { data, isLoading } = useEventDataValuesQuery( + websiteId, + eventName, + filter.propertyName, + { enabled: !isNumeric && !isFreeText }, + ); + + const values = (data as Array<{ value: string }> | undefined)?.map(d => d.value) ?? []; + const filteredValues = search + ? values.filter(v => v.toLowerCase().includes(search.toLowerCase())) + : values; + const selected = filter.value ? filter.value.split(',').filter(Boolean) : []; + + const operatorLabel = (op: string) => { + switch (op) { + case 'eq': return t(labels.is); + case 'neq': return t(labels.isNot); + case 'c': return t(labels.contains); + case 'dnc': return t(labels.doesNotContain); + case 'regex': return t(labels.regexMatch); + case 'notRegex': return t(labels.regexNotMatch); + case 'gt': return t(labels.greaterThan); + case 'lt': return t(labels.lessThan); + case 'gte': return t(labels.greaterThanEquals); + case 'lte': return t(labels.lessThanEquals); + default: return op; + } + }; + + const handleOperatorChange = (op: string) => { + // clear value when switching between multi-select and free-text modes + const wasMulti = filter.operator === 'eq' || filter.operator === 'neq'; + const isMulti = op === 'eq' || op === 'neq'; + onChange({ ...filter, operator: op, value: wasMulti === isMulti ? filter.value : '' }); + }; + + return ( + + + + + + {isNumeric || isFreeText ? ( + onChange({ ...filter, value: v })} + inputMode={isNumeric ? 'numeric' : 'text'} + /> + ) : ( + onChange({ ...filter, value: vals.join(',') })} + searchValue={search} + onSearch={setSearch} + allowSearch + renderValue={vals => (vals.length > 0 ? vals.join(', ') : undefined)} + renderEmptyState={() => + isLoading ? ( + + ) : ( + + ) + } + > + {filteredValues.map(v => ( + + {v} + + ))} + + )} + + + + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataNumericChart.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataNumericChart.tsx index 09b559810..17ef799da 100644 --- a/src/app/(main)/websites/[websiteId]/event-data/EventDataNumericChart.tsx +++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataNumericChart.tsx @@ -14,15 +14,18 @@ import { import { renderDateLabels } from '@/lib/charts'; import { getThemeColors } from '@/lib/colors'; import { generateTimeSeries } from '@/lib/date'; +import type { EventPropertyFilter } from '@/lib/types'; export function EventDataNumericChart({ websiteId, eventName, propertyName, + eventFilters = [], }: { websiteId: string; eventName: string; propertyName: string; + eventFilters?: EventPropertyFilter[]; }) { const { t, labels } = useMessages(); const { theme } = useTheme(); @@ -36,12 +39,14 @@ export function EventDataNumericChart({ eventName, propertyName, 'sum', + eventFilters, ); const avgQuery = useEventDataNumericSeriesQuery( websiteId, eventName, propertyName, 'avg', + eventFilters, ); const chartData: any = useMemo(() => { diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataPivotTable.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataPivotTable.tsx index a0aad0e39..ac8b74dd2 100644 --- a/src/app/(main)/websites/[websiteId]/event-data/EventDataPivotTable.tsx +++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataPivotTable.tsx @@ -8,20 +8,23 @@ import Link from '@/components/common/Link'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { Pager } from '@/components/common/Pager'; import { useEventDataPivotQuery, useEventDataPropertiesQuery, useMessages, useMobile, useNavigation } from '@/components/hooks'; +import type { EventPropertyFilter } from '@/lib/types'; export function EventDataPivotTable({ websiteId, eventName, + eventFilters = [], }: { websiteId: string; eventName: string; + eventFilters?: EventPropertyFilter[]; }) { const { t, labels } = useMessages(); const { router, updateParams } = useNavigation(); const { isMobile } = useMobile(); const propertiesQuery = useEventDataPropertiesQuery(websiteId); - const pivotQuery = useEventDataPivotQuery(websiteId, eventName); + const pivotQuery = useEventDataPivotQuery(websiteId, eventName, eventFilters); const propertyKeys = useMemo(() => { if (!propertiesQuery.data || !eventName) return []; diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataPropertyChart.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataPropertyChart.tsx index 5a7f8e5d1..431b87a6f 100644 --- a/src/app/(main)/websites/[websiteId]/event-data/EventDataPropertyChart.tsx +++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataPropertyChart.tsx @@ -8,7 +8,6 @@ import { LoadingPanel } from '@/components/common/LoadingPanel'; import { useDateRange, useEventDataPropertySeriesQuery, - useEventDataValuesQuery, useLocale, useMessages, useTimezone, @@ -17,26 +16,44 @@ import { ListTable } from '@/components/metrics/ListTable'; import { renderDateLabels } from '@/lib/charts'; import { CHART_COLORS } from '@/lib/constants'; import { generateTimeSeries } from '@/lib/date'; +import type { EventPropertyFilter } from '@/lib/types'; export function EventDataPropertyChart({ websiteId, eventName, propertyName, + eventFilters = [], }: { websiteId: string; eventName: string; propertyName: string; + eventFilters?: EventPropertyFilter[]; }) { const { t, labels } = useMessages(); const { timezone } = useTimezone(); const { dateRange: { startDate, endDate, unit } } = useDateRange({ timezone }); const { locale, dateLocale } = useLocale(); - const { data, isLoading, error } = useEventDataPropertySeriesQuery(websiteId, eventName, propertyName); - const valuesQuery = useEventDataValuesQuery(websiteId, eventName, propertyName); - const valueLabels = useMemo( - () => valuesQuery.data?.map(({ value }) => value) ?? [], - [valuesQuery.data], + const { data, isLoading, isFetching, error } = useEventDataPropertySeriesQuery( + websiteId, + eventName, + propertyName, + eventFilters, ); + + // Aggregate totals per value from the already-filtered time series + const aggregated = useMemo(() => { + if (!data) return []; + const totals = (data as any[]).reduce((obj: Record, { x, y }) => { + obj[x] = (obj[x] ?? 0) + y; + return obj; + }, {}); + return Object.entries(totals) + .map(([value, total]) => ({ value, total: total as number })) + .sort((a, b) => b.total - a.total); + }, [data]); + + const valueLabels = useMemo(() => aggregated.map(({ value }) => value), [aggregated]); + const colorMap = useMemo(() => { return valueLabels.reduce( (obj, label, index) => { @@ -50,13 +67,9 @@ export function EventDataPropertyChart({ const chartData: any = useMemo(() => { if (!data) return; - const map = (data as any[]).reduce((obj, { x, t, y }) => { - if (!obj[x]) { - obj[x] = []; - } - + const map = (data as any[]).reduce((obj: Record, { x, t, y }) => { + if (!obj[x]) obj[x] = []; obj[x].push({ x: t, y }); - return obj; }, {}); @@ -70,57 +83,57 @@ export function EventDataPropertyChart({ }, ], }; - } else { - const keys = [ - ...valueLabels.filter(label => map[label]), - ...Object.keys(map).filter(key => !valueLabels.includes(key)), - ]; - - return { - datasets: keys.map((key, index) => { - const color = colord(colorMap[key] || CHART_COLORS[index % CHART_COLORS.length]); - return { - label: key, - data: generateTimeSeries(map[key], startDate, endDate, unit, dateLocale), - lineTension: 0, - backgroundColor: color.alpha(0.6).toRgbString(), - borderColor: color.alpha(0.7).toRgbString(), - borderWidth: 1, - }; - }), - }; } + + const keys = [ + ...valueLabels.filter(label => map[label]), + ...Object.keys(map).filter(key => !valueLabels.includes(key)), + ]; + + return { + datasets: keys.map((key, index) => { + const color = colord(colorMap[key] || CHART_COLORS[index % CHART_COLORS.length]); + return { + label: key, + data: generateTimeSeries(map[key], startDate, endDate, unit, dateLocale), + lineTension: 0, + backgroundColor: color.alpha(0.6).toRgbString(), + borderColor: color.alpha(0.7).toRgbString(), + borderWidth: 1, + }; + }), + }; }, [data, startDate, endDate, unit, dateLocale, valueLabels, colorMap]); const renderXLabel = useCallback(renderDateLabels(unit, locale), [unit, locale]); - const propertySum = useMemo(() => { - return valuesQuery.data?.reduce((sum, { total }) => sum + total, 0) ?? 0; - }, [valuesQuery.data]); + + const propertySum = useMemo( + () => aggregated.reduce((sum, { total }) => sum + total, 0), + [aggregated], + ); const tableData = useMemo(() => { - if (!valuesQuery.data || propertySum === 0) return []; - - return valuesQuery.data.map(({ value, total }) => ({ + if (!aggregated.length || propertySum === 0) return []; + return aggregated.map(({ value, total }) => ({ label: value, count: total, percent: 100 * (total / propertySum), })); - }, [valuesQuery.data, propertySum]); + }, [aggregated, propertySum]); const pieChartData: any = useMemo(() => { - if (!valuesQuery.data?.length) return null; - + if (!aggregated.length) return null; return { labels: valueLabels, datasets: [ { - data: valuesQuery.data.map(({ total }) => total), + data: aggregated.map(({ total }) => total), backgroundColor: valueLabels.map(label => colorMap[label]), borderWidth: 0, }, ], }; - }, [valuesQuery.data, valueLabels, colorMap]); + }, [aggregated, valueLabels, colorMap]); return ( @@ -139,9 +152,9 @@ export function EventDataPropertyChart({ diff --git a/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx b/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx index 68a1cc5ab..5f3b68e5d 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx @@ -1,8 +1,11 @@ 'use client'; -import { Column, ComboBox, Grid, Label, ListItem, Select } from '@umami/react-zen'; +import { Column, ComboBox, Grid, Label, ListItem, Row, Select } from '@umami/react-zen'; import { useMemo, useState } from 'react'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { useEventDataPropertiesQuery, useMessages } from '@/components/hooks'; +import type { EventPropertyFilter } from '@/lib/types'; +import { EventDataFilterBar } from '../event-data/EventDataFilterBar'; +import { EventDataFilterButton } from '../event-data/EventDataFilterButton'; import { EventDataNumericChart } from '../event-data/EventDataNumericChart'; import { EventDataPivotTable } from '../event-data/EventDataPivotTable'; import { EventDataPropertyChart } from '../event-data/EventDataPropertyChart'; @@ -10,6 +13,7 @@ import { EventDataPropertyChart } from '../event-data/EventDataPropertyChart'; export function EventProperties({ websiteId }: { websiteId: string }) { const [eventName, setEventName] = useState(''); const [propertyName, setPropertyName] = useState(''); + const [eventFilters, setEventFilters] = useState([]); const { t, labels } = useMessages(); const { data, isLoading, isFetching, error } = useEventDataPropertiesQuery(websiteId); @@ -45,6 +49,7 @@ export function EventProperties({ websiteId }: { websiteId: string }) { const handleEventChange = (value: string) => { setEventName(value); setPropertyName(''); + setEventFilters([]); }; return ( @@ -57,39 +62,52 @@ export function EventProperties({ websiteId }: { websiteId: string }) { > {data && ( - - - - - - - - - {properties.map((field: { propertyName: string }) => ( - - {field.propertyName} - - ))} - - - + + + + + + + + + + {properties.map((field: { propertyName: string }) => ( + + {field.propertyName} + + ))} + + + + {eventName && ( + + )} + + )} + {eventName && ( + )} {eventName && propertyName && ( @@ -98,6 +116,7 @@ export function EventProperties({ websiteId }: { websiteId: string }) { websiteId={websiteId} eventName={eventName} propertyName={propertyName} + eventFilters={eventFilters} /> )} {selectedProperty?.dataType === 2 && ( @@ -105,11 +124,18 @@ export function EventProperties({ websiteId }: { websiteId: string }) { websiteId={websiteId} eventName={eventName} propertyName={propertyName} + eventFilters={eventFilters} /> )} )} - {eventName && } + {eventName && ( + + )} ); diff --git a/src/app/api/websites/[websiteId]/event-data-pivot/numeric-series/route.ts b/src/app/api/websites/[websiteId]/event-data-pivot/numeric-series/route.ts index 37e1047f4..048fe7c5c 100644 --- a/src/app/api/websites/[websiteId]/event-data-pivot/numeric-series/route.ts +++ b/src/app/api/websites/[websiteId]/event-data-pivot/numeric-series/route.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { parseEventPropertyFilters } from '@/lib/params'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; import { filterParams } from '@/lib/schema'; @@ -32,7 +33,8 @@ export async function GET( const { eventName, propertyName, metric, ...rest } = query; const filters = await getQueryFilters(rest, websiteId); - const data = await getEventDataNumericSeries(websiteId, eventName, propertyName, metric, filters); + const eventFilters = parseEventPropertyFilters(query); + const data = await getEventDataNumericSeries(websiteId, eventName, propertyName, metric, filters, eventFilters); return json(data); } diff --git a/src/app/api/websites/[websiteId]/event-data-pivot/property-series/route.ts b/src/app/api/websites/[websiteId]/event-data-pivot/property-series/route.ts index 7e923689f..4a60edbef 100644 --- a/src/app/api/websites/[websiteId]/event-data-pivot/property-series/route.ts +++ b/src/app/api/websites/[websiteId]/event-data-pivot/property-series/route.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { parseEventPropertyFilters } from '@/lib/params'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; import { filterParams } from '@/lib/schema'; @@ -31,7 +32,8 @@ export async function GET( const { eventName, propertyName, ...rest } = query; const filters = await getQueryFilters(rest, websiteId); - const data = await getEventDataPropertySeries(websiteId, eventName, propertyName, filters); + const eventFilters = parseEventPropertyFilters(query); + const data = await getEventDataPropertySeries(websiteId, eventName, propertyName, filters, eventFilters); return json(data); } diff --git a/src/app/api/websites/[websiteId]/event-data-pivot/route.ts b/src/app/api/websites/[websiteId]/event-data-pivot/route.ts index 37216f6e3..c43aa9f3f 100644 --- a/src/app/api/websites/[websiteId]/event-data-pivot/route.ts +++ b/src/app/api/websites/[websiteId]/event-data-pivot/route.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { parseEventPropertyFilters } from '@/lib/params'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; import { filterParams, pagingParams } from '@/lib/schema'; @@ -31,7 +32,8 @@ export async function GET( const { eventName, ...rest } = query; const filters = await getQueryFilters(rest, websiteId); - const result = await getEventDataPivot(websiteId, eventName, filters); + const eventFilters = parseEventPropertyFilters(query); + const result = await getEventDataPivot(websiteId, eventName, filters, eventFilters); return json(result); } diff --git a/src/components/hooks/queries/useEventDataNumericSeriesQuery.ts b/src/components/hooks/queries/useEventDataNumericSeriesQuery.ts index eb8b63638..d02674e89 100644 --- a/src/components/hooks/queries/useEventDataNumericSeriesQuery.ts +++ b/src/components/hooks/queries/useEventDataNumericSeriesQuery.ts @@ -1,4 +1,5 @@ -import type { ReactQueryOptions } from '@/lib/types'; +import { serializeEventPropertyFilters } from '@/lib/params'; +import type { EventPropertyFilter, ReactQueryOptions } from '@/lib/types'; import { useApi } from '../useApi'; import { useDateParameters } from '../useDateParameters'; import { useFilterParameters } from '../useFilterParameters'; @@ -8,6 +9,7 @@ export function useEventDataNumericSeriesQuery( eventName: string, propertyName: string, metric: 'sum' | 'avg', + eventFilters: EventPropertyFilter[] = [], options?: ReactQueryOptions, ) { const { get, useQuery } = useApi(); @@ -17,7 +19,7 @@ export function useEventDataNumericSeriesQuery( return useQuery({ queryKey: [ 'websites:event-data-pivot:numeric-series', - { websiteId, eventName, propertyName, metric, startAt, endAt, unit, timezone, ...params }, + { websiteId, eventName, propertyName, metric, eventFilters, startAt, endAt, unit, timezone, ...params }, ], queryFn: () => get(`/websites/${websiteId}/event-data-pivot/numeric-series`, { @@ -28,6 +30,7 @@ export function useEventDataNumericSeriesQuery( endAt, unit, timezone, + ...serializeEventPropertyFilters(eventFilters), ...params, }), enabled: !!(websiteId && eventName && propertyName), diff --git a/src/components/hooks/queries/useEventDataPivotQuery.ts b/src/components/hooks/queries/useEventDataPivotQuery.ts index e805c2b91..e9f689ac7 100644 --- a/src/components/hooks/queries/useEventDataPivotQuery.ts +++ b/src/components/hooks/queries/useEventDataPivotQuery.ts @@ -1,4 +1,5 @@ -import type { ReactQueryOptions } from '@/lib/types'; +import { serializeEventPropertyFilters } from '@/lib/params'; +import type { EventPropertyFilter, ReactQueryOptions } from '@/lib/types'; import { useApi } from '../useApi'; import { useDateParameters } from '../useDateParameters'; import { useFilterParameters } from '../useFilterParameters'; @@ -6,6 +7,7 @@ import { useFilterParameters } from '../useFilterParameters'; export function useEventDataPivotQuery( websiteId: string, eventName: string, + eventFilters: EventPropertyFilter[] = [], options?: ReactQueryOptions, ) { const { get, useQuery } = useApi(); @@ -15,7 +17,7 @@ export function useEventDataPivotQuery( return useQuery({ queryKey: [ 'websites:event-data-pivot', - { websiteId, eventName, startAt, endAt, unit, timezone, ...params }, + { websiteId, eventName, eventFilters, startAt, endAt, unit, timezone, ...params }, ], queryFn: () => get(`/websites/${websiteId}/event-data-pivot`, { @@ -24,6 +26,7 @@ export function useEventDataPivotQuery( endAt, unit, timezone, + ...serializeEventPropertyFilters(eventFilters), ...params, }), enabled: !!(websiteId && eventName), diff --git a/src/components/hooks/queries/useEventDataPropertySeriesQuery.ts b/src/components/hooks/queries/useEventDataPropertySeriesQuery.ts index a60f80442..68d963ba8 100644 --- a/src/components/hooks/queries/useEventDataPropertySeriesQuery.ts +++ b/src/components/hooks/queries/useEventDataPropertySeriesQuery.ts @@ -1,4 +1,5 @@ -import type { ReactQueryOptions } from '@/lib/types'; +import { serializeEventPropertyFilters } from '@/lib/params'; +import type { EventPropertyFilter, ReactQueryOptions } from '@/lib/types'; import { useApi } from '../useApi'; import { useDateParameters } from '../useDateParameters'; import { useFilterParameters } from '../useFilterParameters'; @@ -7,6 +8,7 @@ export function useEventDataPropertySeriesQuery( websiteId: string, eventName: string, propertyName: string, + eventFilters: EventPropertyFilter[] = [], options?: ReactQueryOptions, ) { const { get, useQuery } = useApi(); @@ -16,7 +18,7 @@ export function useEventDataPropertySeriesQuery( return useQuery({ queryKey: [ 'websites:event-data-pivot:property-series', - { websiteId, eventName, propertyName, startAt, endAt, unit, timezone, ...params }, + { websiteId, eventName, propertyName, eventFilters, startAt, endAt, unit, timezone, ...params }, ], queryFn: () => get(`/websites/${websiteId}/event-data-pivot/property-series`, { @@ -26,6 +28,7 @@ export function useEventDataPropertySeriesQuery( endAt, unit, timezone, + ...serializeEventPropertyFilters(eventFilters), ...params, }), enabled: !!(websiteId && eventName && propertyName), diff --git a/src/components/input/FilterBar.tsx b/src/components/input/FilterBar.tsx index 90be256be..c5d6e3c10 100644 --- a/src/components/input/FilterBar.tsx +++ b/src/components/input/FilterBar.tsx @@ -58,8 +58,9 @@ export function FilterBar({ websiteId }: { websiteId?: string }) { justifyContent="space-between" padding="2" backgroundColor="surface-sunken" + wrap="wrap" > - + {segment && !isLoading && ( { theme="dark" > - + {label} diff --git a/src/components/messages.ts b/src/components/messages.ts index d5d77c206..79adab814 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -190,6 +190,7 @@ export const labels: Record = { type: 'label.type', filter: 'label.filter', filters: 'label.filters', + propertyFilter: 'label.property-filter', breakdown: 'label.breakdown', true: 'label.true', false: 'label.false', diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts index 0a8739b6c..5f3bedd0a 100644 --- a/src/lib/clickhouse.ts +++ b/src/lib/clickhouse.ts @@ -1,10 +1,10 @@ +import { CLICKHOUSE } from '@/lib/db'; import { type ClickHouseClient, createClient } from '@clickhouse/client'; import { formatInTimeZone } from 'date-fns-tz'; import debug from 'debug'; -import { CLICKHOUSE } from '@/lib/db'; import { DEFAULT_PAGE_SIZE, FILTER_COLUMNS, OPERATORS } from './constants'; import { filtersObjectToArray } from './params'; -import type { QueryFilters, QueryOptions } from './types'; +import type { EventPropertyFilter, QueryFilters, QueryOptions } from './types'; export const CLICKHOUSE_DATE_FORMATS = { utc: '%Y-%m-%dT%H:%i:%SZ', @@ -235,6 +235,79 @@ function parseFilters(filters: Record, options?: QueryOptions) { }; } +function getEventPropertyFilterQuery(filters: EventPropertyFilter[] = []): { + sql: string; + params: Record; +} { + if (!filters.length) return { sql: '', params: {} }; + + const parts: string[] = []; + const params: Record = {}; + + filters.forEach(({ propertyName, dataType, operator, value }, i) => { + const keyParam = `epf_key_${i}`; + const valParam = `epf_val_${i}`; + params[keyParam] = propertyName; + + const isNumeric = dataType === 2; + const col = isNumeric ? 'number_value' : 'string_value'; + + let condition: string; + if (isNumeric) { + params[valParam] = parseFloat(value) || 0; + const opMap: Record = { + eq: `${col} = {${valParam}:Float64}`, + neq: `${col} != {${valParam}:Float64}`, + gt: `${col} > {${valParam}:Float64}`, + lt: `${col} < {${valParam}:Float64}`, + gte: `${col} >= {${valParam}:Float64}`, + lte: `${col} <= {${valParam}:Float64}`, + }; + condition = opMap[operator] ?? `${col} = {${valParam}:Float64}`; + } else if (operator === 'eq' || operator === 'neq') { + const vals = value.split(',').filter(Boolean); + if (!vals.length) return; + params[valParam] = vals; + condition = mapFilter( + col, + operator === 'eq' ? OPERATORS.equals : OPERATORS.notEquals, + valParam, + 'String', + ); + } else if (operator === 'regex' || operator === 'notRegex') { + if (!value) return; + params[valParam] = value; + condition = mapFilter( + col, + operator === 'regex' ? OPERATORS.regex : OPERATORS.notRegex, + valParam, + 'String', + ); + } else { + if (!value) return; + params[valParam] = value; + condition = mapFilter( + col, + operator === 'c' ? OPERATORS.contains : OPERATORS.doesNotContain, + valParam, + 'String', + ); + } + + parts.push(`and event_id in ( + select event_id + from event_data + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and data_key = {${keyParam}:String} + and data_type = ${dataType} + and ${condition} + )`); + }); + + return { sql: parts.join('\n'), params }; +} + async function pagedRawQuery( query: string, queryParams: Record, @@ -321,6 +394,7 @@ export default { getDateSQL, getSearchSQL, getFilterQuery, + getEventPropertyFilterQuery, getUTCString, parseFilters, pagedRawQuery, diff --git a/src/lib/params.ts b/src/lib/params.ts index f1452254d..60c563f10 100644 --- a/src/lib/params.ts +++ b/src/lib/params.ts @@ -1,5 +1,5 @@ import { FILTER_COLUMNS, OPERATORS } from '@/lib/constants'; -import type { Filter, QueryFilters, QueryOptions } from '@/lib/types'; +import type { EventPropertyFilter, Filter, QueryFilters, QueryOptions } from '@/lib/types'; export function parseFilterValue(param: any) { if (typeof param === 'string') { @@ -88,3 +88,31 @@ export function filtersArrayToObject(filters: Filter[]) { return obj; }, {}); } + +export function parseEventPropertyFilters(query: Record): EventPropertyFilter[] { + return Object.entries(query) + .filter(([key]) => /^epf_/.test(key)) + .flatMap(([key, val]) => { + const dotIndex = (val as string).indexOf('.'); + if (dotIndex < 1) return []; + const withoutPrefix = key.slice(4); // strip "epf_" + const propertyName = withoutPrefix.replace(/\d+$/, ''); // strip trailing index digits + const operator = (val as string).slice(0, dotIndex); + const value = (val as string).slice(dotIndex + 1); + const isNumeric = + ['gt', 'lt', 'gte', 'lte'].includes(operator) || + (['eq', 'neq'].includes(operator) && value !== '' && !Number.isNaN(Number(value))); + return [{ propertyName, dataType: isNumeric ? 2 : 1, operator, value }]; + }); +} + +export function serializeEventPropertyFilters(filters: EventPropertyFilter[]): Record { + const counts: Record = {}; + return Object.fromEntries( + filters.map(f => { + const n = counts[f.propertyName] ?? 0; + counts[f.propertyName] = n + 1; + return [`epf_${f.propertyName}${n > 0 ? n : ''}`, `${f.operator}.${f.value}`]; + }), + ); +} diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index c3b6aaca3..266f11455 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -1,10 +1,10 @@ +import { PrismaClient } from '@/generated/prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; import { readReplicas } from '@prisma/extension-read-replicas'; import debug from 'debug'; -import { PrismaClient } from '@/generated/prisma/client'; import { DEFAULT_PAGE_SIZE, FILTER_COLUMNS, OPERATORS, SESSION_COLUMNS } from './constants'; import { filtersObjectToArray } from './params'; -import type { Operator, QueryFilters, QueryOptions } from './types'; +import type { EventPropertyFilter, Operator, QueryFilters, QueryOptions } from './types'; const log = debug('umami:prisma'); @@ -252,6 +252,68 @@ function parseFilters(filters: Record, options?: QueryOptions) { }; } +function getEventPropertyFilterQuery(filters: EventPropertyFilter[] = []): { + sql: string; + params: Record; +} { + if (!filters.length) return { sql: '', params: {} }; + + const parts: string[] = []; + const params: Record = {}; + + filters.forEach(({ propertyName, dataType, operator, value }, i) => { + const keyParam = `epf_key_${i}`; + const valParam = `epf_val_${i}`; + params[keyParam] = propertyName; + + const isNumeric = dataType === 2; + const col = isNumeric ? 'cast(number_value as decimal)' : 'string_value'; + + let condition: string; + if (isNumeric) { + params[valParam] = parseFloat(value) || 0; + const opMap: Record = { + eq: `${col} = {{${valParam}}}`, + neq: `${col} != {{${valParam}}}`, + gt: `${col} > {{${valParam}}}`, + lt: `${col} < {{${valParam}}}`, + gte: `${col} >= {{${valParam}}}`, + lte: `${col} <= {{${valParam}}}`, + }; + condition = opMap[operator] ?? `${col} = {{${valParam}}}`; + } else if (operator === 'eq' || operator === 'neq') { + const vals = value.split(',').filter(Boolean); + if (!vals.length) return; + params[valParam] = vals; + condition = + operator === 'eq' + ? `${col} = ANY({{${valParam}::text[]}})` + : `${col} != ALL({{${valParam}::text[]}})`; + } else if (operator === 'regex' || operator === 'notRegex') { + if (!value) return; + params[valParam] = value; + condition = operator === 'regex' ? `${col} ~* {{${valParam}}}` : `${col} !~* {{${valParam}}}`; + } else { + if (!value) return; + params[valParam] = `%${value}%`; + condition = + operator === 'c' ? `${col} ilike {{${valParam}}}` : `${col} not ilike {{${valParam}}}`; + } + + parts.push(`and website_event.event_id in ( + select website_event_id + from event_data + where website_id = {{websiteId::uuid}} + and created_at between {{startDate}} and {{endDate}} + and data_key = {{${keyParam}}} + and data_type = ${dataType} + and ${condition} + )`); + }); + + return { sql: parts.join('\n'), params }; +} + async function rawQuery(sql: string, data: Record, name?: string): Promise { if (process.env.LOG_QUERY) { log('QUERY:\n', sql); @@ -426,6 +488,7 @@ export default { getDateSQL, getDateWeeklySQL, getFilterQuery, + getEventPropertyFilterQuery, getSearchParameters, getTimestampDiffSQL, getSearchSQL, diff --git a/src/lib/request.ts b/src/lib/request.ts index 69f6eb1d7..40ffabe06 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -1,5 +1,3 @@ -import { startOfMonth, subMonths } from 'date-fns'; -import { z } from 'zod'; import { checkAuth } from '@/lib/auth'; import { DEFAULT_PAGE_SIZE, FILTER_COLUMNS, OPERATORS } from '@/lib/constants'; import { getAllowedUnits, getMinimumUnit, maxDate, parseDateRange } from '@/lib/date'; @@ -8,6 +6,8 @@ import { filtersArrayToObject } from '@/lib/params'; import { badRequest, unauthorized } from '@/lib/response'; import type { QueryFilters } from '@/lib/types'; import { getWebsiteSegment } from '@/queries/prisma'; +import { startOfMonth, subMonths } from 'date-fns'; +import { z } from 'zod'; export async function parseRequest( request: Request, @@ -30,9 +30,9 @@ export async function parseRequest( } else if (isGet) { query = result.data; - // Re-add suffixed filter params (e.g., browser1, os2) stripped by Zod schema + // Re-add dynamic params stripped by Zod schema: suffixed filter params (browser1, os2) for (const key of Object.keys(rawQuery)) { - if (/\d+$/.test(key) && !(key in query)) { + if ((/\d+$/.test(key) || /^epf_/.test(key)) && !(key in query)) { query[key] = rawQuery[key]; } } diff --git a/src/lib/types.ts b/src/lib/types.ts index 72104352e..7569f2821 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -30,6 +30,13 @@ export interface Auth { }; } +export interface EventPropertyFilter { + propertyName: string; + dataType: number; + operator: string; + value: string; +} + export interface Filter { name: string; operator: Operator; diff --git a/src/queries/sql/events/getEventDataNumericSeries.ts b/src/queries/sql/events/getEventDataNumericSeries.ts index 5b0ef85d2..64bd9b66e 100644 --- a/src/queries/sql/events/getEventDataNumericSeries.ts +++ b/src/queries/sql/events/getEventDataNumericSeries.ts @@ -1,7 +1,7 @@ import clickhouse from '@/lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; -import type { QueryFilters } from '@/lib/types'; +import type { EventPropertyFilter, QueryFilters } from '@/lib/types'; const FUNCTION_NAME = 'getEventDataNumericSeries'; @@ -12,6 +12,7 @@ export async function getEventDataNumericSeries( propertyName: string, metric: 'sum' | 'avg', filters: QueryFilters, + eventFilters?: EventPropertyFilter[], ] ) { return runQuery({ @@ -26,13 +27,15 @@ async function relationalQuery( propertyName: string, metric: 'sum' | 'avg', filters: QueryFilters, + eventFilters: EventPropertyFilter[] = [], ) { const { timezone = 'utc', unit = 'day' } = filters; - const { rawQuery, getDateSQL, parseFilters } = prisma; + const { rawQuery, getDateSQL, parseFilters, getEventPropertyFilterQuery } = prisma; const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, websiteId, }); + const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters); const aggFn = metric === 'avg' ? 'avg' : 'sum'; return rawQuery( @@ -53,10 +56,11 @@ async function relationalQuery( and event_data.data_key = {{propertyName}} and event_data.data_type = 2 ${filterQuery} + ${epfSQL} group by 1 order by 1 `, - { ...queryParams, eventName, propertyName }, + { ...queryParams, eventName, propertyName, ...epfParams }, FUNCTION_NAME, ); } @@ -67,10 +71,12 @@ async function clickhouseQuery( propertyName: string, metric: 'sum' | 'avg', filters: QueryFilters, + eventFilters: EventPropertyFilter[] = [], ): Promise<{ t: string; y: number }[]> { const { timezone = 'UTC', unit = 'day' } = filters; - const { rawQuery, getDateSQL, parseFilters } = clickhouse; + const { rawQuery, getDateSQL, parseFilters, getEventPropertyFilterQuery } = clickhouse; const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId }); + const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters); const aggFn = metric === 'avg' ? 'avg' : 'sum'; return rawQuery( @@ -95,10 +101,11 @@ async function clickhouseQuery( and event_data.data_key = {propertyName:String} and event_data.data_type = 2 ${filterQuery} + ${epfSQL} group by t order by t `, - { ...queryParams, eventName, propertyName }, + { ...queryParams, eventName, propertyName, ...epfParams }, FUNCTION_NAME, ); } diff --git a/src/queries/sql/events/getEventDataPivot.ts b/src/queries/sql/events/getEventDataPivot.ts index eb69fa410..00044b649 100644 --- a/src/queries/sql/events/getEventDataPivot.ts +++ b/src/queries/sql/events/getEventDataPivot.ts @@ -2,12 +2,17 @@ import clickhouse from '@/lib/clickhouse'; import { DEFAULT_PAGE_SIZE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; -import type { QueryFilters } from '@/lib/types'; +import type { EventPropertyFilter, QueryFilters } from '@/lib/types'; const FUNCTION_NAME = 'getEventDataPivot'; export async function getEventDataPivot( - ...args: [websiteId: string, eventName: string, filters: QueryFilters] + ...args: [ + websiteId: string, + eventName: string, + filters: QueryFilters, + eventFilters?: EventPropertyFilter[], + ] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -15,8 +20,8 @@ export async function getEventDataPivot( }); } -async function relationalQuery(websiteId: string, eventName: string, filters: QueryFilters) { - const { rawQuery, parseFilters } = prisma; +async function relationalQuery(websiteId: string, eventName: string, filters: QueryFilters, eventFilters: EventPropertyFilter[] = []) { + const { rawQuery, parseFilters, getEventPropertyFilterQuery } = prisma; const { page = 1, pageSize } = filters; const size = +pageSize || DEFAULT_PAGE_SIZE; const offset = +size * (+page - 1); @@ -25,6 +30,7 @@ async function relationalQuery(websiteId: string, eventName: string, filters: Qu ...filters, websiteId, }); + const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters); const countResult = await rawQuery( ` @@ -39,8 +45,9 @@ async function relationalQuery(websiteId: string, eventName: string, filters: Qu and website_event.created_at between {{startDate}} and {{endDate}} and website_event.event_name = {{eventName}} ${filterQuery} + ${epfSQL} `, - { ...queryParams, eventName }, + { ...queryParams, eventName, ...epfParams }, ); const count = countResult[0].num; @@ -59,6 +66,7 @@ async function relationalQuery(websiteId: string, eventName: string, filters: Qu and website_event.created_at between {{startDate}} and {{endDate}} and website_event.event_name = {{eventName}} ${filterQuery} + ${epfSQL} group by website_event.event_id order by max(website_event.created_at) desc limit ${size} offset ${offset} @@ -85,7 +93,7 @@ async function relationalQuery(websiteId: string, eventName: string, filters: Qu and event_data.created_at between {{startDate}} and {{endDate}} order by website_event.created_at desc `, - { ...queryParams, eventName }, + { ...queryParams, eventName, ...epfParams }, FUNCTION_NAME, ); @@ -106,13 +114,14 @@ async function relationalQuery(websiteId: string, eventName: string, filters: Qu return { data: [...eventMap.values()], count, page: +page, pageSize: size }; } -async function clickhouseQuery(websiteId: string, eventName: string, filters: QueryFilters) { - const { rawQuery, parseFilters } = clickhouse; +async function clickhouseQuery(websiteId: string, eventName: string, filters: QueryFilters, eventFilters: EventPropertyFilter[] = []) { + const { rawQuery, parseFilters, getEventPropertyFilterQuery } = clickhouse; const { page = 1, pageSize } = filters; const size = +pageSize || DEFAULT_PAGE_SIZE; const offset = +size * (+page - 1); const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId }); + const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters); const count = await rawQuery( ` @@ -132,8 +141,9 @@ async function clickhouseQuery(websiteId: string, eventName: string, filters: Qu where event_data_pivot.website_id = {websiteId:UUID} and event_data_pivot.created_at between {startDate:DateTime64} and {endDate:DateTime64} ${filterQuery} + ${epfSQL} `, - { ...queryParams, eventName }, + { ...queryParams, eventName, ...epfParams }, ).then((res: any) => res[0].num); const data = await rawQuery( @@ -161,11 +171,12 @@ async function clickhouseQuery(websiteId: string, eventName: string, filters: Qu where event_data_pivot.website_id = {websiteId:UUID} and event_data_pivot.created_at between {startDate:DateTime64} and {endDate:DateTime64} ${filterQuery} + ${epfSQL} group by event_id, session_id, event_name, url_path, created_at order by created_at desc limit ${size} offset ${offset} `, - { ...queryParams, eventName }, + { ...queryParams, eventName, ...epfParams }, FUNCTION_NAME, ); diff --git a/src/queries/sql/events/getEventDataPropertySeries.ts b/src/queries/sql/events/getEventDataPropertySeries.ts index deb1c65b9..c8ef193dc 100644 --- a/src/queries/sql/events/getEventDataPropertySeries.ts +++ b/src/queries/sql/events/getEventDataPropertySeries.ts @@ -1,12 +1,18 @@ import clickhouse from '@/lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; -import type { QueryFilters } from '@/lib/types'; +import type { EventPropertyFilter, QueryFilters } from '@/lib/types'; const FUNCTION_NAME = 'getEventDataPropertySeries'; export async function getEventDataPropertySeries( - ...args: [websiteId: string, eventName: string, propertyName: string, filters: QueryFilters] + ...args: [ + websiteId: string, + eventName: string, + propertyName: string, + filters: QueryFilters, + eventFilters?: EventPropertyFilter[], + ] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -19,13 +25,15 @@ async function relationalQuery( eventName: string, propertyName: string, filters: QueryFilters, + eventFilters: EventPropertyFilter[] = [], ) { const { timezone = 'utc', unit = 'day' } = filters; - const { rawQuery, getDateSQL, parseFilters } = prisma; + const { rawQuery, getDateSQL, parseFilters, getEventPropertyFilterQuery } = prisma; const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, websiteId, }); + const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters); return rawQuery( ` @@ -46,10 +54,11 @@ async function relationalQuery( and event_data.data_key = {{propertyName}} and event_data.data_type = 1 ${filterQuery} + ${epfSQL} group by 1, 2 order by 2 `, - { ...queryParams, eventName, propertyName }, + { ...queryParams, eventName, propertyName, ...epfParams }, FUNCTION_NAME, ); } @@ -59,10 +68,12 @@ async function clickhouseQuery( eventName: string, propertyName: string, filters: QueryFilters, + eventFilters: EventPropertyFilter[] = [], ): Promise<{ x: string; t: string; y: number }[]> { const { timezone = 'UTC', unit = 'day' } = filters; - const { rawQuery, getDateSQL, parseFilters } = clickhouse; + const { rawQuery, getDateSQL, parseFilters, getEventPropertyFilterQuery } = clickhouse; const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId }); + const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters); return rawQuery( ` @@ -87,10 +98,11 @@ async function clickhouseQuery( and event_data.data_key = {propertyName:String} and event_data.data_type = 1 ${filterQuery} + ${epfSQL} group by x, t order by t `, - { ...queryParams, eventName, propertyName }, + { ...queryParams, eventName, propertyName, ...epfParams }, FUNCTION_NAME, ); } From 43fb351ac7c8462114568de16bff7fd931696317 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 24 Apr 2026 13:48:28 -0700 Subject: [PATCH 46/59] add logic for booleans, dates, arrays --- .../migrations/11_add_event_data_pivot.sql | 4 +- db/clickhouse/schema.sql | 4 +- .../event-data/EventDataFilterBar.tsx | 21 +++--- .../event-data/EventDataFilterEditForm.tsx | 6 +- .../event-data/EventDataFilterRecord.tsx | 68 +++++++++++++------ src/lib/clickhouse.ts | 47 +++++++------ src/lib/params.ts | 33 +++++++-- src/lib/prisma.ts | 46 +++++++++---- src/lib/types.ts | 2 +- 9 files changed, 153 insertions(+), 78 deletions(-) diff --git a/db/clickhouse/migrations/11_add_event_data_pivot.sql b/db/clickhouse/migrations/11_add_event_data_pivot.sql index 09020457d..15ba1a7e8 100644 --- a/db/clickhouse/migrations/11_add_event_data_pivot.sql +++ b/db/clickhouse/migrations/11_add_event_data_pivot.sql @@ -26,7 +26,7 @@ AS SELECT created_at, groupArrayState(data_key) AS property_keys, groupArrayState(multiIf( - data_type = 1, ifNull(string_value, ''), + data_type IN (1, 3, 5), ifNull(string_value, ''), data_type = 2, toString(ifNull(number_value, 0)), data_type = 4, toString(ifNull(date_value, toDateTime(0))), '' @@ -46,7 +46,7 @@ SELECT created_at, groupArrayState(data_key), groupArrayState(multiIf( - data_type = 1, ifNull(string_value, ''), + data_type IN (1, 3, 5), ifNull(string_value, ''), data_type = 2, toString(ifNull(number_value, 0)), data_type = 4, toString(ifNull(date_value, toDateTime(0))), '' diff --git a/db/clickhouse/schema.sql b/db/clickhouse/schema.sql index ddcf54d46..0540c97b2 100644 --- a/db/clickhouse/schema.sql +++ b/db/clickhouse/schema.sql @@ -336,11 +336,11 @@ AS SELECT created_at, groupArrayState(data_key) AS property_keys, groupArrayState(multiIf( - data_type = 1, ifNull(string_value, ''), + data_type IN (1, 3, 5), ifNull(string_value, ''), data_type = 2, toString(ifNull(number_value, 0)), data_type = 4, toString(ifNull(date_value, toDateTime(0))), '' )) AS property_values, groupArrayState(data_type) AS property_types FROM umami.event_data -GROUP BY website_id, session_id, event_id, event_name, url_path, created_at; \ No newline at end of file +GROUP BY website_id, session_id, event_id, event_name, url_path, created_at; diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx index a424d595a..cec4ed408 100644 --- a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx +++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx @@ -2,6 +2,7 @@ import { Button, Icon, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen'; import { useMessages } from '@/components/hooks'; import { X } from '@/components/icons'; +import { OPERATORS } from '@/lib/constants'; import type { EventPropertyFilter } from '@/lib/types'; export function EventDataFilterBar({ @@ -17,16 +18,16 @@ export function EventDataFilterBar({ const operatorLabel = (op: string) => { switch (op) { - case 'eq': return t(labels.is); - case 'neq': return t(labels.isNot); - case 'c': return t(labels.contains); - case 'dnc': return t(labels.doesNotContain); - case 'regex': return t(labels.regexMatch); - case 'notRegex': return t(labels.regexNotMatch); - case 'gt': return t(labels.greaterThan); - case 'lt': return t(labels.lessThan); - case 'gte': return t(labels.greaterThanEquals); - case 'lte': return t(labels.lessThanEquals); + case OPERATORS.equals: return t(labels.is); + case OPERATORS.notEquals: return t(labels.isNot); + case OPERATORS.contains: return t(labels.contains); + case OPERATORS.doesNotContain: return t(labels.doesNotContain); + case OPERATORS.regex: return t(labels.regexMatch); + case OPERATORS.notRegex: return t(labels.regexNotMatch); + case OPERATORS.greaterThan: return t(labels.greaterThan); + case OPERATORS.lessThan: return t(labels.lessThan); + case OPERATORS.greaterThanEquals: return t(labels.greaterThanEquals); + case OPERATORS.lessThanEquals: return t(labels.lessThanEquals); default: return op; } }; diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterEditForm.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterEditForm.tsx index 82030cd32..5d8c1f960 100644 --- a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterEditForm.tsx +++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterEditForm.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import { Empty } from '@/components/common/Empty'; import { useEventDataFieldsQuery, useMessages, useMobile } from '@/components/hooks'; import { Plus } from '@/components/icons'; +import { OPERATORS } from '@/lib/constants'; import type { EventPropertyFilter } from '@/lib/types'; import { EventDataFilterRecord } from './EventDataFilterRecord'; @@ -29,7 +30,10 @@ export function EventDataFilterEditForm({ const handleAdd = (propertyName: string) => { const field = (fields as any[]).find(f => f.propertyName === propertyName); const dataType: number = field?.dataType ?? 1; - setFilters(prev => [...prev, { propertyName, dataType, operator: 'eq', value: '' }]); + setFilters(prev => [ + ...prev, + { propertyName, dataType, operator: OPERATORS.equals, value: '' }, + ]); }; const handleChange = (index: number, filter: EventPropertyFilter) => { diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterRecord.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterRecord.tsx index 4ca358029..16097db1d 100644 --- a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterRecord.tsx +++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterRecord.tsx @@ -5,10 +5,32 @@ import { Empty } from '@/components/common/Empty'; import { MultiSelect } from '@/components/common/MultiSelect'; import { useEventDataValuesQuery, useMessages } from '@/components/hooks'; import { X } from '@/components/icons'; -import type { EventPropertyFilter } from '@/lib/types'; +import { OPERATORS } from '@/lib/constants'; +import type { EventPropertyFilter, Operator } from '@/lib/types'; -const STRING_OPERATORS = ['eq', 'neq', 'c', 'dnc', 'regex', 'notRegex'] as const; -const NUMERIC_OPERATORS = ['eq', 'neq', 'gt', 'lt', 'gte', 'lte'] as const; +const STRING_OPERATORS: Operator[] = [ + OPERATORS.equals, + OPERATORS.notEquals, + OPERATORS.contains, + OPERATORS.doesNotContain, + OPERATORS.regex, + OPERATORS.notRegex, +]; +const NUMERIC_OPERATORS: Operator[] = [ + OPERATORS.equals, + OPERATORS.notEquals, + OPERATORS.greaterThan, + OPERATORS.lessThan, + OPERATORS.greaterThanEquals, + OPERATORS.lessThanEquals, +]; +const MULTI_OPERATORS: Operator[] = [OPERATORS.equals, OPERATORS.notEquals]; +const FREE_TEXT_OPERATORS: Operator[] = [ + OPERATORS.contains, + OPERATORS.doesNotContain, + OPERATORS.regex, + OPERATORS.notRegex, +]; export function EventDataFilterRecord({ websiteId, @@ -28,7 +50,7 @@ export function EventDataFilterRecord({ const isNumeric = filter.dataType === 2; const operators = isNumeric ? NUMERIC_OPERATORS : STRING_OPERATORS; - const isFreeText = filter.operator === 'c' || filter.operator === 'dnc' || filter.operator === 'regex' || filter.operator === 'notRegex'; + const isFreeText = FREE_TEXT_OPERATORS.includes(filter.operator); const { data, isLoading } = useEventDataValuesQuery( websiteId, @@ -43,27 +65,31 @@ export function EventDataFilterRecord({ : values; const selected = filter.value ? filter.value.split(',').filter(Boolean) : []; - const operatorLabel = (op: string) => { + const operatorLabel = (op: Operator) => { switch (op) { - case 'eq': return t(labels.is); - case 'neq': return t(labels.isNot); - case 'c': return t(labels.contains); - case 'dnc': return t(labels.doesNotContain); - case 'regex': return t(labels.regexMatch); - case 'notRegex': return t(labels.regexNotMatch); - case 'gt': return t(labels.greaterThan); - case 'lt': return t(labels.lessThan); - case 'gte': return t(labels.greaterThanEquals); - case 'lte': return t(labels.lessThanEquals); + case OPERATORS.equals: return t(labels.is); + case OPERATORS.notEquals: return t(labels.isNot); + case OPERATORS.contains: return t(labels.contains); + case OPERATORS.doesNotContain: return t(labels.doesNotContain); + case OPERATORS.regex: return t(labels.regexMatch); + case OPERATORS.notRegex: return t(labels.regexNotMatch); + case OPERATORS.greaterThan: return t(labels.greaterThan); + case OPERATORS.lessThan: return t(labels.lessThan); + case OPERATORS.greaterThanEquals: return t(labels.greaterThanEquals); + case OPERATORS.lessThanEquals: return t(labels.lessThanEquals); default: return op; } }; - const handleOperatorChange = (op: string) => { + const handleOperatorChange = (op: Operator) => { // clear value when switching between multi-select and free-text modes - const wasMulti = filter.operator === 'eq' || filter.operator === 'neq'; - const isMulti = op === 'eq' || op === 'neq'; - onChange({ ...filter, operator: op, value: wasMulti === isMulti ? filter.value : '' }); + const wasMulti = MULTI_OPERATORS.includes(filter.operator); + const isMulti = MULTI_OPERATORS.includes(op); + onChange({ + ...filter, + operator: op, + value: wasMulti === isMulti ? filter.value : '', + }); }; return ( @@ -71,8 +97,8 @@ export function EventDataFilterRecord({ - handleOperatorChange(value as Operator)}> + {operators.map(op => ( {operatorLabel(op)} diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts index 5f3bedd0a..efeaa3b84 100644 --- a/src/lib/clickhouse.ts +++ b/src/lib/clickhouse.ts @@ -4,7 +4,7 @@ import { formatInTimeZone } from 'date-fns-tz'; import debug from 'debug'; import { DEFAULT_PAGE_SIZE, FILTER_COLUMNS, OPERATORS } from './constants'; import { filtersObjectToArray } from './params'; -import type { EventPropertyFilter, QueryFilters, QueryOptions } from './types'; +import type { EventPropertyFilter, Operator, QueryFilters, QueryOptions } from './types'; export const CLICKHOUSE_DATE_FORMATS = { utc: '%Y-%m-%dT%H:%i:%SZ', @@ -18,10 +18,19 @@ export const CLICKHOUSE_DATE_FORMATS = { const log = debug('umami:clickhouse'); +const EQUALITY_OPERATORS: Operator[] = [OPERATORS.equals, OPERATORS.notEquals]; +const REGEX_OPERATORS: Operator[] = [OPERATORS.regex, OPERATORS.notRegex]; + let clickhouse: ClickHouseClient; const enabled = Boolean(process.env.CLICKHOUSE_URL); function getClient() { + const clickhouseUrl = process.env.CLICKHOUSE_URL; + + if (!clickhouseUrl) { + throw new Error('CLICKHOUSE_URL is not set.'); + } + const { hostname, port, @@ -29,7 +38,7 @@ function getClient() { protocol, username = 'default', password, - } = new URL(process.env.CLICKHOUSE_URL); + } = new URL(clickhouseUrl); const client = createClient({ url: `${protocol}//${hostname}:${port}`, @@ -70,13 +79,7 @@ function getSearchSQL(column: string, param: string = 'search'): string { return `and positionCaseInsensitive(${column}, {${param}:String}) > 0`; } -function mapFilter( - column: string, - operator: string, - name: string, - type: string = 'String', - paramName?: string, -) { +function mapFilter(column: string, operator: Operator, name: string, type: string = 'String', paramName?: string) { const param = paramName ?? name; const value = `{${param}:${type}}`; @@ -210,7 +213,7 @@ function getQueryParams(filters: Record) { const key = paramName ?? name; - obj[key] = ([OPERATORS.equals, OPERATORS.notEquals] as string[]).includes(operator) + obj[key] = EQUALITY_OPERATORS.includes(operator) ? Array.isArray(value) ? value : [value] @@ -256,30 +259,30 @@ function getEventPropertyFilterQuery(filters: EventPropertyFilter[] = []): { if (isNumeric) { params[valParam] = parseFloat(value) || 0; const opMap: Record = { - eq: `${col} = {${valParam}:Float64}`, - neq: `${col} != {${valParam}:Float64}`, - gt: `${col} > {${valParam}:Float64}`, - lt: `${col} < {${valParam}:Float64}`, - gte: `${col} >= {${valParam}:Float64}`, - lte: `${col} <= {${valParam}:Float64}`, + [OPERATORS.equals]: `${col} = {${valParam}:Float64}`, + [OPERATORS.notEquals]: `${col} != {${valParam}:Float64}`, + [OPERATORS.greaterThan]: `${col} > {${valParam}:Float64}`, + [OPERATORS.lessThan]: `${col} < {${valParam}:Float64}`, + [OPERATORS.greaterThanEquals]: `${col} >= {${valParam}:Float64}`, + [OPERATORS.lessThanEquals]: `${col} <= {${valParam}:Float64}`, }; condition = opMap[operator] ?? `${col} = {${valParam}:Float64}`; - } else if (operator === 'eq' || operator === 'neq') { + } else if (EQUALITY_OPERATORS.includes(operator)) { const vals = value.split(',').filter(Boolean); if (!vals.length) return; params[valParam] = vals; condition = mapFilter( col, - operator === 'eq' ? OPERATORS.equals : OPERATORS.notEquals, + operator === OPERATORS.equals ? OPERATORS.equals : OPERATORS.notEquals, valParam, 'String', ); - } else if (operator === 'regex' || operator === 'notRegex') { + } else if (REGEX_OPERATORS.includes(operator)) { if (!value) return; params[valParam] = value; condition = mapFilter( col, - operator === 'regex' ? OPERATORS.regex : OPERATORS.notRegex, + operator === OPERATORS.regex ? OPERATORS.regex : OPERATORS.notRegex, valParam, 'String', ); @@ -288,7 +291,7 @@ function getEventPropertyFilterQuery(filters: EventPropertyFilter[] = []): { params[valParam] = value; condition = mapFilter( col, - operator === 'c' ? OPERATORS.contains : OPERATORS.doesNotContain, + operator === OPERATORS.contains ? OPERATORS.contains : OPERATORS.doesNotContain, valParam, 'String', ); @@ -379,7 +382,7 @@ async function findFirst(data: any[]) { async function connect() { if (enabled && !clickhouse) { - clickhouse = process.env.CLICKHOUSE_URL && (globalThis[CLICKHOUSE] || getClient()); + clickhouse = globalThis[CLICKHOUSE] || getClient(); } return clickhouse; diff --git a/src/lib/params.ts b/src/lib/params.ts index 60c563f10..0f3c7f905 100644 --- a/src/lib/params.ts +++ b/src/lib/params.ts @@ -1,5 +1,22 @@ import { FILTER_COLUMNS, OPERATORS } from '@/lib/constants'; -import type { EventPropertyFilter, Filter, QueryFilters, QueryOptions } from '@/lib/types'; +import type { EventPropertyFilter, Filter, Operator, QueryFilters, QueryOptions } from '@/lib/types'; + +const VALID_OPERATORS: Operator[] = Object.values(OPERATORS); +const NUMERIC_EVENT_PROPERTY_OPERATORS: Operator[] = [ + OPERATORS.greaterThan, + OPERATORS.lessThan, + OPERATORS.greaterThanEquals, + OPERATORS.lessThanEquals, +]; +const EQUALITY_OPERATORS: Operator[] = [OPERATORS.equals, OPERATORS.notEquals]; + +function resolveOperator(value?: string): Operator | undefined { + if (!value) { + return undefined; + } + + return VALID_OPERATORS.find(operator => operator === value); +} export function parseFilterValue(param: any) { if (typeof param === 'string') { @@ -9,7 +26,7 @@ export function parseFilterValue(param: any) { const [, operator, value] = param.match(regex) || []; - const resolvedOperator = operator || OPERATORS.equals; + const resolvedOperator = resolveOperator(operator) ?? OPERATORS.equals; const resolvedValue = value ?? param; if (resolvedOperator === OPERATORS.equals || resolvedOperator === OPERATORS.notEquals) { @@ -97,11 +114,17 @@ export function parseEventPropertyFilters(query: Record): EventProp if (dotIndex < 1) return []; const withoutPrefix = key.slice(4); // strip "epf_" const propertyName = withoutPrefix.replace(/\d+$/, ''); // strip trailing index digits - const operator = (val as string).slice(0, dotIndex); + const rawOperator = (val as string).slice(0, dotIndex); + const operator = resolveOperator(rawOperator); + if (!operator) { + return []; + } const value = (val as string).slice(dotIndex + 1); const isNumeric = - ['gt', 'lt', 'gte', 'lte'].includes(operator) || - (['eq', 'neq'].includes(operator) && value !== '' && !Number.isNaN(Number(value))); + NUMERIC_EVENT_PROPERTY_OPERATORS.includes(operator) || + (EQUALITY_OPERATORS.includes(operator) && + value !== '' && + !Number.isNaN(Number(value))); return [{ propertyName, dataType: isNumeric ? 2 : 1, operator, value }]; }); } diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 266f11455..422069159 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -8,6 +8,10 @@ import type { EventPropertyFilter, Operator, QueryFilters, QueryOptions } from ' const log = debug('umami:prisma'); +const EQUALITY_OPERATORS: Operator[] = [OPERATORS.equals, OPERATORS.notEquals]; +const SEARCH_OPERATORS: Operator[] = [OPERATORS.contains, OPERATORS.doesNotContain]; +const REGEX_OPERATORS: Operator[] = [OPERATORS.regex, OPERATORS.notRegex]; + const PRISMA = 'prisma'; const PRISMA_LOG_OPTIONS = { @@ -216,9 +220,9 @@ function getQueryParams(filters: Record) { const key = paramName ?? name; - if (([OPERATORS.contains, OPERATORS.doesNotContain] as Operator[]).includes(operator)) { + if (SEARCH_OPERATORS.includes(operator)) { obj[key] = `%${value}%`; - } else if (([OPERATORS.equals, OPERATORS.notEquals] as Operator[]).includes(operator)) { + } else if (EQUALITY_OPERATORS.includes(operator)) { obj[key] = Array.isArray(value) ? value : [value]; } else { obj[key] = value; @@ -273,31 +277,34 @@ function getEventPropertyFilterQuery(filters: EventPropertyFilter[] = []): { if (isNumeric) { params[valParam] = parseFloat(value) || 0; const opMap: Record = { - eq: `${col} = {{${valParam}}}`, - neq: `${col} != {{${valParam}}}`, - gt: `${col} > {{${valParam}}}`, - lt: `${col} < {{${valParam}}}`, - gte: `${col} >= {{${valParam}}}`, - lte: `${col} <= {{${valParam}}}`, + [OPERATORS.equals]: `${col} = {{${valParam}}}`, + [OPERATORS.notEquals]: `${col} != {{${valParam}}}`, + [OPERATORS.greaterThan]: `${col} > {{${valParam}}}`, + [OPERATORS.lessThan]: `${col} < {{${valParam}}}`, + [OPERATORS.greaterThanEquals]: `${col} >= {{${valParam}}}`, + [OPERATORS.lessThanEquals]: `${col} <= {{${valParam}}}`, }; condition = opMap[operator] ?? `${col} = {{${valParam}}}`; - } else if (operator === 'eq' || operator === 'neq') { + } else if (EQUALITY_OPERATORS.includes(operator)) { const vals = value.split(',').filter(Boolean); if (!vals.length) return; params[valParam] = vals; condition = - operator === 'eq' + operator === OPERATORS.equals ? `${col} = ANY({{${valParam}::text[]}})` : `${col} != ALL({{${valParam}::text[]}})`; - } else if (operator === 'regex' || operator === 'notRegex') { + } else if (REGEX_OPERATORS.includes(operator)) { if (!value) return; params[valParam] = value; - condition = operator === 'regex' ? `${col} ~* {{${valParam}}}` : `${col} !~* {{${valParam}}}`; + condition = + operator === OPERATORS.regex ? `${col} ~* {{${valParam}}}` : `${col} !~* {{${valParam}}}`; } else { if (!value) return; params[valParam] = `%${value}%`; condition = - operator === 'c' ? `${col} ilike {{${valParam}}}` : `${col} not ilike {{${valParam}}}`; + operator === OPERATORS.contains + ? `${col} ilike {{${valParam}}}` + : `${col} not ilike {{${valParam}}}`; } parts.push(`and website_event.event_id in ( @@ -424,7 +431,13 @@ function transaction(input: any, options?: any) { } function getSchema() { - const connectionUrl = new URL(process.env.DATABASE_URL); + const databaseUrl = process.env.DATABASE_URL; + + if (!databaseUrl) { + throw new Error('DATABASE_URL is not set.'); + } + + const connectionUrl = new URL(databaseUrl); return connectionUrl.searchParams.get('schema'); } @@ -433,6 +446,11 @@ function getClient() { const url = process.env.DATABASE_URL; const replicaUrl = process.env.DATABASE_REPLICA_URL; const logQuery = process.env.LOG_QUERY; + + if (!url) { + throw new Error('DATABASE_URL is not set.'); + } + const schema = getSchema(); const baseAdapter = new PrismaPg({ connectionString: url }, { schema }); diff --git a/src/lib/types.ts b/src/lib/types.ts index 7569f2821..2f74a4fd2 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -33,7 +33,7 @@ export interface Auth { export interface EventPropertyFilter { propertyName: string; dataType: number; - operator: string; + operator: Operator; value: string; } From 5a1ef5e674b69f7e670120f14bc35d16137c023c Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 24 Apr 2026 15:04:59 -0700 Subject: [PATCH 47/59] add event-data support and charts for arrays, booleans, dates --- .../event-data/EventDataDateChart.tsx | 373 ++++++++++++++++++ .../event-data/EventDataFilterBar.tsx | 62 ++- .../event-data/EventDataNumericChart.tsx | 76 +++- .../event-data/EventDataPropertyChart.tsx | 35 +- .../[websiteId]/events/EventProperties.tsx | 60 ++- .../event-data-pivot/array-series/route.ts | 41 ++ .../event-data-pivot/date-series/route.ts | 40 ++ .../event-data-pivot/numeric-series/route.ts | 6 +- .../event-data-pivot/numeric-stats/route.ts | 39 ++ .../event-data-pivot/property-series/route.ts | 4 +- src/components/hooks/index.ts | 3 + .../queries/useEventDataArraySeriesQuery.ts | 47 +++ .../queries/useEventDataDateSeriesQuery.ts | 45 +++ .../queries/useEventDataNumericSeriesQuery.ts | 2 +- .../queries/useEventDataNumericStatsQuery.ts | 36 ++ .../useEventDataPropertySeriesQuery.ts | 4 +- src/lib/clickhouse.ts | 2 +- src/lib/prisma.ts | 14 + src/lib/types.ts | 19 + .../sql/events/getEventDataArraySeries.ts | 110 ++++++ .../sql/events/getEventDataDateSeries.ts | 107 +++++ .../sql/events/getEventDataNumericSeries.ts | 20 +- .../sql/events/getEventDataNumericStats.ts | 106 +++++ .../sql/events/getEventDataPropertySeries.ts | 13 +- src/queries/sql/index.ts | 3 + 25 files changed, 1205 insertions(+), 62 deletions(-) create mode 100644 src/app/(main)/websites/[websiteId]/event-data/EventDataDateChart.tsx create mode 100644 src/app/api/websites/[websiteId]/event-data-pivot/array-series/route.ts create mode 100644 src/app/api/websites/[websiteId]/event-data-pivot/date-series/route.ts create mode 100644 src/app/api/websites/[websiteId]/event-data-pivot/numeric-stats/route.ts create mode 100644 src/components/hooks/queries/useEventDataArraySeriesQuery.ts create mode 100644 src/components/hooks/queries/useEventDataDateSeriesQuery.ts create mode 100644 src/components/hooks/queries/useEventDataNumericStatsQuery.ts create mode 100644 src/queries/sql/events/getEventDataArraySeries.ts create mode 100644 src/queries/sql/events/getEventDataDateSeries.ts create mode 100644 src/queries/sql/events/getEventDataNumericStats.ts diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataDateChart.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataDateChart.tsx new file mode 100644 index 000000000..136d73aa0 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataDateChart.tsx @@ -0,0 +1,373 @@ +'use client'; +import { Column, Grid, Text, useTheme } from '@umami/react-zen'; +import { colord } from 'colord'; +import { + differenceInCalendarDays, + format, + formatDistance, + isValid, + startOfDay, + startOfMonth, + startOfWeek, + startOfYear, + type Locale, +} from 'date-fns'; +import { toZonedTime } from 'date-fns-tz'; +import { type ReactNode, useCallback, useMemo } from 'react'; +import { BarChart } from '@/components/charts/BarChart'; +import { PieChart } from '@/components/charts/PieChart'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { useEventDataDateSeriesQuery, useLocale, useMessages, useTimezone } from '@/components/hooks'; +import { ListTable } from '@/components/metrics/ListTable'; +import { MetricsBar } from '@/components/metrics/MetricsBar'; +import { renderDateLabels } from '@/lib/charts'; +import { getThemeColors } from '@/lib/colors'; +import { CHART_COLORS } from '@/lib/constants'; +import { formatDate, generateTimeSeries } from '@/lib/date'; +import { formatLongNumber } from '@/lib/format'; +import type { EventDataDateSeriesPoint, EventPropertyFilter } from '@/lib/types'; + +type DateChartUnit = 'day' | 'week' | 'month' | 'year'; +type ParsedDateRow = EventDataDateSeriesPoint & { date: Date }; + +function parseDateValue(value: string) { + const normalized = value?.trim().replace(' ', 'T').replace(/Z$/, '').replace(/\.\d+$/, ''); + const [datePart = '', timePart = '00:00:00'] = normalized.split('T'); + const [year = 0, month = 1, day = 1] = datePart.split('-').map(Number); + const [hour = 0, minute = 0, second = 0] = timePart.split(':').map(Number); + const date = new Date(year, month - 1, day, hour, minute, second); + + return isValid(date) ? date : null; +} + +function InsightCard({ + label, + value, + hint, +}: { + label: string; + value: string; + hint?: ReactNode; +}) { + return ( + + + {label} + + + {value} + + {hint && ( + + {hint} + + )} + + ); +} + +function getChartUnit(minDate: Date, maxDate: Date): DateChartUnit { + const spanDays = differenceInCalendarDays(maxDate, minDate); + + if (spanDays <= 45) return 'day'; + if (spanDays <= 180) return 'week'; + if (spanDays <= 730) return 'month'; + + return 'year'; +} + +function getBucketStart(date: Date, unit: DateChartUnit, dateLocale: Locale) { + switch (unit) { + case 'week': + return startOfWeek(date, { locale: dateLocale }); + case 'month': + return startOfMonth(date); + case 'year': + return startOfYear(date); + default: + return startOfDay(date); + } +} + +export function EventDataDateChart({ + websiteId, + eventName, + propertyName, + eventFilters = [], +}: { + websiteId: string; + eventName: string; + propertyName: string; + eventFilters?: EventPropertyFilter[]; +}) { + const { theme } = useTheme(); + const { t, labels } = useMessages(); + const { locale, dateLocale } = useLocale(); + const { timezone } = useTimezone(); + const { colors } = useMemo(() => getThemeColors(theme), [theme]); + const query = useEventDataDateSeriesQuery(websiteId, eventName, propertyName, eventFilters); + + const rows = useMemo(() => query.data ?? [], [query.data]); + const total = useMemo(() => rows.reduce((sum, { y }) => sum + y, 0), [rows]); + const zonedNow = useMemo(() => toZonedTime(new Date(), timezone), [timezone]); + + const exactDateRows = useMemo(() => { + return rows + .map(row => { + const date = parseDateValue(row.t); + + return date ? { ...row, date } : null; + }) + .filter((row): row is ParsedDateRow => row !== null) + .sort((a, b) => a.date.getTime() - b.date.getTime()); + }, [rows]); + + const minPropertyDate = exactDateRows[0]?.date ?? null; + const lastExactDateRow = exactDateRows[exactDateRows.length - 1]; + const maxPropertyDate = lastExactDateRow?.date ?? null; + + const chartUnit = useMemo( + () => + minPropertyDate && maxPropertyDate + ? getChartUnit(minPropertyDate, maxPropertyDate) + : 'day', + [minPropertyDate, maxPropertyDate], + ); + + const chartRows = useMemo(() => { + if (!exactDateRows.length) return []; + + const buckets = exactDateRows.reduce( + (obj, row) => { + const bucketStart = getBucketStart(row.date, chartUnit, dateLocale); + const bucketKey = format(bucketStart, "yyyy-MM-dd'T'HH:mm:ss"); + const existing = obj[bucketKey]; + + obj[bucketKey] = { + t: bucketKey, + y: (existing?.y ?? 0) + row.y, + }; + + return obj; + }, + {} as Record, + ); + + return Object.values(buckets).sort( + (a, b) => (parseDateValue(a.t)?.getTime() ?? 0) - (parseDateValue(b.t)?.getTime() ?? 0), + ); + }, [exactDateRows, chartUnit, dateLocale]); + + const chartData: any = useMemo(() => { + if (!rows.length || !minPropertyDate || !maxPropertyDate) return null; + + return { + datasets: [ + { + label: t(labels.count), + data: generateTimeSeries( + chartRows.map(({ t, y }) => ({ x: t, y })), + minPropertyDate, + maxPropertyDate, + chartUnit, + dateLocale, + ), + barPercentage: 0.9, + categoryPercentage: 0.9, + ...colors.chart.visitors, + borderWidth: 1, + }, + ], + }; + }, [ + rows, + minPropertyDate, + maxPropertyDate, + chartRows, + chartUnit, + dateLocale, + colors, + t, + labels.count, + ]); + + const renderXLabel = useCallback(renderDateLabels(chartUnit, locale), [chartUnit, locale]); + + const weekdayTableData = useMemo(() => { + if (!exactDateRows.length || total === 0) return []; + + const weekStartsOn = dateLocale.options?.weekStartsOn ?? 0; + const weekdayOrder = Array.from( + { length: 7 }, + (_, index) => (weekStartsOn + index) % 7, + ); + const weekdayTotals = exactDateRows.reduce( + (obj, { date, y }) => { + const day = date.getDay(); + obj[day] = (obj[day] ?? 0) + y; + return obj; + }, + {} as Record, + ); + + return weekdayOrder.map(day => { + const referenceDate = new Date(2026, 0, 4 + day); + const count = weekdayTotals[day] ?? 0; + + return { + label: format(referenceDate, 'EEEE', { locale: dateLocale }), + count, + percent: total ? 100 * (count / total) : 0, + }; + }); + }, [exactDateRows, total, dateLocale]); + + const topWeekday = useMemo( + () => [...weekdayTableData].sort((a, b) => b.count - a.count)[0], + [weekdayTableData], + ); + const weekdayLabels = useMemo( + () => weekdayTableData.map(({ label }) => label), + [weekdayTableData], + ); + const weekdayColorMap = useMemo(() => { + return weekdayLabels.reduce( + (obj, label, index) => { + const color = colord(CHART_COLORS[index % CHART_COLORS.length]); + obj[label] = color.alpha(0.7).toRgbString(); + return obj; + }, + {} as Record, + ); + }, [weekdayLabels]); + const weekdayPieChartData = useMemo(() => { + if (!weekdayTableData.length) return null; + + return { + labels: weekdayLabels, + datasets: [ + { + data: weekdayTableData.map(({ count }) => count), + backgroundColor: weekdayLabels.map(label => weekdayColorMap[label]), + borderWidth: 0, + }, + ], + }; + }, [weekdayTableData, weekdayLabels, weekdayColorMap]); + const spanDays = useMemo( + () => + minPropertyDate && maxPropertyDate + ? differenceInCalendarDays(maxPropertyDate, minPropertyDate) + 1 + : 0, + [minPropertyDate, maxPropertyDate], + ); + + return ( + + + + + + + + + + + + + {chartData && minPropertyDate && maxPropertyDate && ( + formatLongNumber(Number(value))} + height="400px" + /> + )} + + + + + + + + {weekdayPieChartData && ( + + )} + + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx index cec4ed408..41568558b 100644 --- a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx +++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx @@ -1,9 +1,9 @@ 'use client'; -import { Button, Icon, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen'; import { useMessages } from '@/components/hooks'; import { X } from '@/components/icons'; import { OPERATORS } from '@/lib/constants'; import type { EventPropertyFilter } from '@/lib/types'; +import { Button, Icon, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen'; export function EventDataFilterBar({ filters, @@ -18,17 +18,28 @@ export function EventDataFilterBar({ const operatorLabel = (op: string) => { switch (op) { - case OPERATORS.equals: return t(labels.is); - case OPERATORS.notEquals: return t(labels.isNot); - case OPERATORS.contains: return t(labels.contains); - case OPERATORS.doesNotContain: return t(labels.doesNotContain); - case OPERATORS.regex: return t(labels.regexMatch); - case OPERATORS.notRegex: return t(labels.regexNotMatch); - case OPERATORS.greaterThan: return t(labels.greaterThan); - case OPERATORS.lessThan: return t(labels.lessThan); - case OPERATORS.greaterThanEquals: return t(labels.greaterThanEquals); - case OPERATORS.lessThanEquals: return t(labels.lessThanEquals); - default: return op; + case OPERATORS.equals: + return t(labels.is); + case OPERATORS.notEquals: + return t(labels.isNot); + case OPERATORS.contains: + return t(labels.contains); + case OPERATORS.doesNotContain: + return t(labels.doesNotContain); + case OPERATORS.regex: + return t(labels.regexMatch); + case OPERATORS.notRegex: + return t(labels.regexNotMatch); + case OPERATORS.greaterThan: + return t(labels.greaterThan); + case OPERATORS.lessThan: + return t(labels.lessThan); + case OPERATORS.greaterThanEquals: + return t(labels.greaterThanEquals); + case OPERATORS.lessThanEquals: + return t(labels.lessThanEquals); + default: + return op; } }; @@ -37,7 +48,15 @@ export function EventDataFilterBar({ }; return ( - + {filters.map((filter, index) => ( void; }) { return ( - - + + {label} diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataNumericChart.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataNumericChart.tsx index 17ef799da..94da88f82 100644 --- a/src/app/(main)/websites/[websiteId]/event-data/EventDataNumericChart.tsx +++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataNumericChart.tsx @@ -1,19 +1,22 @@ 'use client'; -import { Column } from '@umami/react-zen'; -import { useTheme } from '@umami/react-zen'; +import { Column, useTheme } from '@umami/react-zen'; import { useCallback, useMemo } from 'react'; import { BarChart } from '@/components/charts/BarChart'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { useDateRange, useEventDataNumericSeriesQuery, + useEventDataNumericStatsQuery, useLocale, useMessages, useTimezone, } from '@/components/hooks'; +import { MetricCard } from '@/components/metrics/MetricCard'; +import { MetricsBar } from '@/components/metrics/MetricsBar'; import { renderDateLabels } from '@/lib/charts'; import { getThemeColors } from '@/lib/colors'; import { generateTimeSeries } from '@/lib/date'; +import { formatLongNumber } from '@/lib/format'; import type { EventPropertyFilter } from '@/lib/types'; export function EventDataNumericChart({ @@ -48,13 +51,35 @@ export function EventDataNumericChart({ 'avg', eventFilters, ); + const statsQuery = useEventDataNumericStatsQuery( + websiteId, + eventName, + propertyName, + eventFilters, + ); + + const sumRows = useMemo( + () => (sumQuery.data as { t: string; y: number }[] | undefined) ?? [], + [sumQuery.data], + ); + const avgRows = useMemo( + () => (avgQuery.data as { t: string; y: number }[] | undefined) ?? [], + [avgQuery.data], + ); + const stats = statsQuery.data; + + const formatMetricValue = useCallback( + (n: number) => + Number(n).toLocaleString(locale, { + maximumFractionDigits: 2, + minimumFractionDigits: Number.isInteger(Number(n)) ? 0 : 2, + }), + [locale], + ); const chartData: any = useMemo(() => { if (!sumQuery.data && !avgQuery.data) return; - const sumRows = (sumQuery.data as { t: string; y: number }[] | undefined) ?? []; - const avgRows = (avgQuery.data as { t: string; y: number }[] | undefined) ?? []; - return { datasets: [ { @@ -89,15 +114,50 @@ export function EventDataNumericChart({ }, ], }; - }, [sumQuery.data, avgQuery.data, t, labels, startDate, endDate, unit, dateLocale, colors]); + }, [sumQuery.data, avgQuery.data, t, labels, startDate, endDate, unit, dateLocale, colors, sumRows, avgRows]); const renderXLabel = useCallback(renderDateLabels(unit, locale), [unit, locale]); return ( + + + + + + + + + {chartData && ( diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataPropertyChart.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataPropertyChart.tsx index 431b87a6f..0fccff7c0 100644 --- a/src/app/(main)/websites/[websiteId]/event-data/EventDataPropertyChart.tsx +++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataPropertyChart.tsx @@ -6,6 +6,7 @@ import { BarChart } from '@/components/charts/BarChart'; import { PieChart } from '@/components/charts/PieChart'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { + useEventDataArraySeriesQuery, useDateRange, useEventDataPropertySeriesQuery, useLocale, @@ -16,34 +17,46 @@ import { ListTable } from '@/components/metrics/ListTable'; import { renderDateLabels } from '@/lib/charts'; import { CHART_COLORS } from '@/lib/constants'; import { generateTimeSeries } from '@/lib/date'; -import type { EventPropertyFilter } from '@/lib/types'; +import type { EventDataSeriesPoint, EventPropertyFilter } from '@/lib/types'; export function EventDataPropertyChart({ websiteId, eventName, propertyName, eventFilters = [], + seriesType = 'property', }: { websiteId: string; eventName: string; propertyName: string; eventFilters?: EventPropertyFilter[]; + seriesType?: 'property' | 'array'; }) { const { t, labels } = useMessages(); const { timezone } = useTimezone(); const { dateRange: { startDate, endDate, unit } } = useDateRange({ timezone }); const { locale, dateLocale } = useLocale(); - const { data, isLoading, isFetching, error } = useEventDataPropertySeriesQuery( + const propertySeriesQuery = useEventDataPropertySeriesQuery( websiteId, eventName, propertyName, eventFilters, + { enabled: seriesType === 'property' }, ); + const arraySeriesQuery = useEventDataArraySeriesQuery( + websiteId, + eventName, + propertyName, + eventFilters, + { enabled: seriesType === 'array' }, + ); + const { data, isLoading, isFetching, error } = + seriesType === 'array' ? arraySeriesQuery : propertySeriesQuery; // Aggregate totals per value from the already-filtered time series const aggregated = useMemo(() => { if (!data) return []; - const totals = (data as any[]).reduce((obj: Record, { x, y }) => { + const totals = data.reduce((obj: Record, { x, y }) => { obj[x] = (obj[x] ?? 0) + y; return obj; }, {}); @@ -64,12 +77,12 @@ export function EventDataPropertyChart({ ); }, [valueLabels]); - const chartData: any = useMemo(() => { + const chartData = useMemo(() => { if (!data) return; - const map = (data as any[]).reduce((obj: Record, { x, t, y }) => { + const map = data.reduce((obj: Record, { x, t, y }) => { if (!obj[x]) obj[x] = []; - obj[x].push({ x: t, y }); + obj[x].push({ x, t, y }); return obj; }, {}); @@ -95,7 +108,13 @@ export function EventDataPropertyChart({ const color = colord(colorMap[key] || CHART_COLORS[index % CHART_COLORS.length]); return { label: key, - data: generateTimeSeries(map[key], startDate, endDate, unit, dateLocale), + data: generateTimeSeries( + map[key].map(({ t, y }) => ({ x: t, y })), + startDate, + endDate, + unit, + dateLocale, + ), lineTension: 0, backgroundColor: color.alpha(0.6).toRgbString(), borderColor: color.alpha(0.7).toRgbString(), @@ -121,7 +140,7 @@ export function EventDataPropertyChart({ })); }, [aggregated, propertySum]); - const pieChartData: any = useMemo(() => { + const pieChartData = useMemo(() => { if (!aggregated.length) return null; return { labels: valueLabels, diff --git a/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx b/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx index 5f3b68e5d..4fe4297a3 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx @@ -3,7 +3,9 @@ import { Column, ComboBox, Grid, Label, ListItem, Row, Select } from '@umami/rea import { useMemo, useState } from 'react'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { useEventDataPropertiesQuery, useMessages } from '@/components/hooks'; +import { DATA_TYPE } from '@/lib/constants'; import type { EventPropertyFilter } from '@/lib/types'; +import { EventDataDateChart } from '../event-data/EventDataDateChart'; import { EventDataFilterBar } from '../event-data/EventDataFilterBar'; import { EventDataFilterButton } from '../event-data/EventDataFilterButton'; import { EventDataNumericChart } from '../event-data/EventDataNumericChart'; @@ -62,8 +64,20 @@ export function EventProperties({ websiteId }: { websiteId: string }) { > {data && ( - - + + - {isNumeric || isFreeText ? ( + {isBoolean ? ( + + ) : isDate ? ( + onChange({ ...filter, value })} + /> + ) : isArray ? ( + { + setSearch(v); + onChange({ ...filter, value: v }); + }} + formValue="text" + allowsEmptyCollection + allowsCustomValue + renderEmptyState={() => + isLoading ? ( + + ) : ( + + ) + } + > + {filteredValues.map(v => ( + + {v} + + ))} + + ) : isNumeric || isFreeText ? ( onChange({ ...filter, value: v })} @@ -145,3 +205,62 @@ export function EventDataFilterRecord({ ); } + +function DateValuePicker({ + value, + onChange, +}: { + value: string; + onChange: (value: string) => void; +}) { + const { t, labels } = useMessages(); + const [showPicker, setShowPicker] = useState(false); + const selectedDate = getSelectedDate(value); + const [draftDate, setDraftDate] = useState(selectedDate); + + const handleOpen = () => { + setDraftDate(getSelectedDate(value)); + setShowPicker(true); + }; + + const handleApply = () => { + onChange(format(draftDate, 'yyyy-MM-dd')); + setShowPicker(false); + }; + + return ( + + + + + + + + + + + + + + + ); +} + +function getSelectedDate(value?: string) { + if (!value) { + return new Date(); + } + + const date = parseISO(value); + + return Number.isNaN(date.getTime()) ? new Date() : date; +} diff --git a/src/app/api/websites/[websiteId]/event-data/values/route.ts b/src/app/api/websites/[websiteId]/event-data/values/route.ts index 12e8f2dc0..4f1d8fc62 100644 --- a/src/app/api/websites/[websiteId]/event-data/values/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/values/route.ts @@ -14,6 +14,7 @@ export async function GET( endAt: z.coerce.number().int(), event: z.string(), propertyName: z.string(), + dataType: z.coerce.number().int().optional(), ...filterParams, }); @@ -29,12 +30,13 @@ export async function GET( return unauthorized(); } - const { propertyName } = query; + const { propertyName, dataType } = query; const filters = await getQueryFilters(query, websiteId); const data = await getEventDataValues(websiteId, { ...filters, propertyName, + dataType, }); return json(data); diff --git a/src/components/hooks/queries/useEventDataValuesQuery.ts b/src/components/hooks/queries/useEventDataValuesQuery.ts index db2a2d7e0..3141e41d1 100644 --- a/src/components/hooks/queries/useEventDataValuesQuery.ts +++ b/src/components/hooks/queries/useEventDataValuesQuery.ts @@ -7,6 +7,7 @@ export function useEventDataValuesQuery( websiteId: string, event: string, propertyName: string, + dataType?: number, options?: ReactQueryOptions, ) { const { get, useQuery } = useApi(); @@ -16,7 +17,7 @@ export function useEventDataValuesQuery( return useQuery({ queryKey: [ 'websites:event-data:values', - { websiteId, startAt, endAt, unit, timezone, ...filters, event, propertyName }, + { websiteId, startAt, endAt, unit, timezone, ...filters, event, propertyName, dataType }, ], queryFn: () => get(`/websites/${websiteId}/event-data/values`, { @@ -27,6 +28,7 @@ export function useEventDataValuesQuery( ...filters, event, propertyName, + dataType, }), enabled: !!(websiteId && propertyName), ...options, diff --git a/src/components/input/DateFilter.tsx b/src/components/input/DateFilter.tsx index ca55bbefa..4bff007ad 100644 --- a/src/components/input/DateFilter.tsx +++ b/src/components/input/DateFilter.tsx @@ -1,10 +1,9 @@ import { Dialog, ListItem, ListSeparator, Modal, Select, type SelectProps } from '@umami/react-zen'; -import { endOfYear } from 'date-fns'; import { Fragment, type Key, useState } from 'react'; import { DateDisplay } from '@/components/common/DateDisplay'; import { useMessages, useMobile } from '@/components/hooks'; import { DatePickerForm } from '@/components/metrics/DatePickerForm'; -import { parseDateRange } from '@/lib/date'; +import { getMaxSelectableDate, parseDateRange } from '@/lib/date'; export interface DateFilterProps extends SelectProps { value?: string; @@ -129,7 +128,7 @@ export function DateFilter({ startDate={startDate} endDate={endDate} minDate={new Date(2000, 0, 1)} - maxDate={endOfYear(new Date())} + maxDate={getMaxSelectableDate()} onChange={handlePickerChange} onClose={() => setShowPicker(false)} /> diff --git a/src/components/input/FilterBar.tsx b/src/components/input/FilterBar.tsx index c5d6e3c10..d8c121222 100644 --- a/src/components/input/FilterBar.tsx +++ b/src/components/input/FilterBar.tsx @@ -14,6 +14,7 @@ import { useFilters, useFormat, useMessages, + useMobile, useNavigation, useWebsiteSegmentQuery, } from '@/components/hooks'; @@ -22,6 +23,7 @@ import { isSearchOperator } from '@/lib/params'; export function FilterBar({ websiteId }: { websiteId?: string }) { const { t, labels } = useMessages(); + const { isMobile } = useMobile(); const { formatValue } = useFormat(); const { router, @@ -114,8 +116,19 @@ export function FilterBar({ websiteId }: { websiteId?: string }) { )} - - + + {({ close }) => { return ; }} diff --git a/src/components/input/SegmentSaveButton.tsx b/src/components/input/SegmentSaveButton.tsx index 9acf3ded8..520d5eeaa 100644 --- a/src/components/input/SegmentSaveButton.tsx +++ b/src/components/input/SegmentSaveButton.tsx @@ -1,10 +1,11 @@ import { Button, Dialog, DialogTrigger, Icon, Modal, Text } from '@umami/react-zen'; import { SegmentEditForm } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditForm'; -import { useMessages } from '@/components/hooks'; +import { useMessages, useMobile } from '@/components/hooks'; import { Plus } from '@/components/icons'; export function SegmentSaveButton({ websiteId }: { websiteId: string }) { const { t, labels } = useMessages(); + const { isMobile } = useMobile(); return ( @@ -14,8 +15,18 @@ export function SegmentSaveButton({ websiteId }: { websiteId: string }) { {t(labels.segment)} - - + + {({ close }) => { return ; }} diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts index 3329a2b75..14773a0e2 100644 --- a/src/lib/clickhouse.ts +++ b/src/lib/clickhouse.ts @@ -2,7 +2,7 @@ import { CLICKHOUSE } from '@/lib/db'; import { type ClickHouseClient, createClient } from '@clickhouse/client'; import { formatInTimeZone } from 'date-fns-tz'; import debug from 'debug'; -import { DEFAULT_PAGE_SIZE, FILTER_COLUMNS, OPERATORS } from './constants'; +import { DATA_TYPE, DEFAULT_PAGE_SIZE, FILTER_COLUMNS, OPERATORS } from './constants'; import { filtersObjectToArray } from './params'; import type { EventPropertyFilter, Operator, QueryFilters, QueryOptions } from './types'; @@ -238,7 +238,10 @@ function parseFilters(filters: Record, options?: QueryOptions) { }; } -function getEventPropertyFilterQuery(filters: EventPropertyFilter[] = []): { +function getEventPropertyFilterQuery( + filters: EventPropertyFilter[] = [], + timezone?: string, +): { sql: string; params: Record; } { @@ -252,49 +255,78 @@ function getEventPropertyFilterQuery(filters: EventPropertyFilter[] = []): { const valParam = `epf_val_${i}`; params[keyParam] = propertyName; - const isNumeric = dataType === 2; - const col = isNumeric ? 'number_value' : 'string_value'; - let condition: string; - if (isNumeric) { - params[valParam] = parseFloat(value) || 0; - const opMap: Record = { - [OPERATORS.equals]: `${col} = {${valParam}:Float64}`, - [OPERATORS.notEquals]: `${col} != {${valParam}:Float64}`, - [OPERATORS.greaterThan]: `${col} > {${valParam}:Float64}`, - [OPERATORS.lessThan]: `${col} < {${valParam}:Float64}`, - [OPERATORS.greaterThanEquals]: `${col} >= {${valParam}:Float64}`, - [OPERATORS.lessThanEquals]: `${col} <= {${valParam}:Float64}`, - }; - condition = opMap[operator] ?? `${col} = {${valParam}:Float64}`; - } else if (EQUALITY_OPERATORS.includes(operator)) { - const vals = value.split(',').filter(Boolean); - if (!vals.length) return; - params[valParam] = vals; - condition = mapFilter( - col, - operator === OPERATORS.equals ? OPERATORS.equals : OPERATORS.notEquals, - valParam, - 'String', - ); - } else if (REGEX_OPERATORS.includes(operator)) { - if (!value) return; - params[valParam] = value; - condition = mapFilter( - col, - operator === OPERATORS.regex ? OPERATORS.regex : OPERATORS.notRegex, - valParam, - 'String', - ); - } else { - if (!value) return; - params[valParam] = value; - condition = mapFilter( - col, - operator === OPERATORS.contains ? OPERATORS.contains : OPERATORS.doesNotContain, - valParam, - 'String', - ); + switch (dataType) { + case DATA_TYPE.number: { + const col = 'number_value'; + params[valParam] = parseFloat(value) || 0; + const opMap: Record = { + [OPERATORS.equals]: `${col} = {${valParam}:Float64}`, + [OPERATORS.notEquals]: `${col} != {${valParam}:Float64}`, + [OPERATORS.greaterThan]: `${col} > {${valParam}:Float64}`, + [OPERATORS.lessThan]: `${col} < {${valParam}:Float64}`, + [OPERATORS.greaterThanEquals]: `${col} >= {${valParam}:Float64}`, + [OPERATORS.lessThanEquals]: `${col} <= {${valParam}:Float64}`, + }; + condition = opMap[operator] ?? `${col} = {${valParam}:Float64}`; + break; + } + case DATA_TYPE.date: { + if (!value) return; + params[valParam] = value; + const dateCol = timezone + ? `toDate(toTimezone(date_value, {timezone:String}))` + : `toDate(toTimezone(date_value, 'UTC'))`; + const opMap: Record = { + [OPERATORS.before]: `${dateCol} < {${valParam}:Date}`, + [OPERATORS.after]: `${dateCol} > {${valParam}:Date}`, + }; + condition = opMap[operator] ?? `${dateCol} = {${valParam}:Date}`; + break; + } + case DATA_TYPE.array: { + if (!value) return; + params[valParam] = value; + condition = + operator === OPERATORS.contains + ? `has(JSONExtract(ifNull(string_value, '[]'), 'Array(String)'), {${valParam}:String})` + : `not has(JSONExtract(ifNull(string_value, '[]'), 'Array(String)'), {${valParam}:String})`; + break; + } + default: { + const col = 'string_value'; + + if (EQUALITY_OPERATORS.includes(operator)) { + const vals = value.split(',').filter(Boolean); + if (!vals.length) return; + params[valParam] = vals; + condition = mapFilter( + col, + operator === OPERATORS.equals ? OPERATORS.equals : OPERATORS.notEquals, + valParam, + 'String', + ); + } else if (REGEX_OPERATORS.includes(operator)) { + if (!value) return; + params[valParam] = value; + condition = mapFilter( + col, + operator === OPERATORS.regex ? OPERATORS.regex : OPERATORS.notRegex, + valParam, + 'String', + ); + } else { + if (!value) return; + params[valParam] = value; + condition = mapFilter( + col, + operator === OPERATORS.contains ? OPERATORS.contains : OPERATORS.doesNotContain, + valParam, + 'String', + ); + } + break; + } } parts.push(`and event_id in ( diff --git a/src/lib/date.ts b/src/lib/date.ts index ae5ce2a76..82fba23f8 100644 --- a/src/lib/date.ts +++ b/src/lib/date.ts @@ -113,6 +113,10 @@ export function normalizeTimezone(timezone: string): string { return TIMEZONE_MAPPINGS[timezone] || timezone; } +export function getMaxSelectableDate(now = new Date()) { + return max([endOfYear(now), addMonths(now, 6)]); +} + export function isValidTimezone(timezone: string) { try { const normalizedTimezone = normalizeTimezone(timezone); diff --git a/src/lib/params.ts b/src/lib/params.ts index 0f3c7f905..6f430ddbe 100644 --- a/src/lib/params.ts +++ b/src/lib/params.ts @@ -1,14 +1,8 @@ -import { FILTER_COLUMNS, OPERATORS } from '@/lib/constants'; +import { DATA_TYPE, FILTER_COLUMNS, OPERATORS } from '@/lib/constants'; import type { EventPropertyFilter, Filter, Operator, QueryFilters, QueryOptions } from '@/lib/types'; const VALID_OPERATORS: Operator[] = Object.values(OPERATORS); -const NUMERIC_EVENT_PROPERTY_OPERATORS: Operator[] = [ - OPERATORS.greaterThan, - OPERATORS.lessThan, - OPERATORS.greaterThanEquals, - OPERATORS.lessThanEquals, -]; -const EQUALITY_OPERATORS: Operator[] = [OPERATORS.equals, OPERATORS.notEquals]; +const VALID_EVENT_DATA_TYPES = Object.values(DATA_TYPE); function resolveOperator(value?: string): Operator | undefined { if (!value) { @@ -110,22 +104,33 @@ export function parseEventPropertyFilters(query: Record): EventProp return Object.entries(query) .filter(([key]) => /^epf_/.test(key)) .flatMap(([key, val]) => { - const dotIndex = (val as string).indexOf('.'); - if (dotIndex < 1) return []; + const stringValue = String(val); const withoutPrefix = key.slice(4); // strip "epf_" const propertyName = withoutPrefix.replace(/\d+$/, ''); // strip trailing index digits - const rawOperator = (val as string).slice(0, dotIndex); + const prefixedDotMatch = stringValue.match(/^(\d+)\.([^.]+)\.(.*)$/); + const untypedDotMatch = stringValue.match(/^([^.]+)\.(.*)$/); + const explicitDataType = prefixedDotMatch ? Number(prefixedDotMatch[1]) : undefined; + const rawOperator = prefixedDotMatch ? prefixedDotMatch[2] : untypedDotMatch?.[1]; const operator = resolveOperator(rawOperator); - if (!operator) { + + if (!operator || (explicitDataType !== undefined && !VALID_EVENT_DATA_TYPES.includes(explicitDataType))) { return []; } - const value = (val as string).slice(dotIndex + 1); - const isNumeric = - NUMERIC_EVENT_PROPERTY_OPERATORS.includes(operator) || - (EQUALITY_OPERATORS.includes(operator) && - value !== '' && - !Number.isNaN(Number(value))); - return [{ propertyName, dataType: isNumeric ? 2 : 1, operator, value }]; + + const value = prefixedDotMatch ? prefixedDotMatch[3] : untypedDotMatch?.[2]; + + if (value === undefined) { + return []; + } + + return [ + { + propertyName, + dataType: explicitDataType ?? DATA_TYPE.string, + operator, + value, + }, + ]; }); } @@ -135,7 +140,7 @@ export function serializeEventPropertyFilters(filters: EventPropertyFilter[]): R filters.map(f => { const n = counts[f.propertyName] ?? 0; counts[f.propertyName] = n + 1; - return [`epf_${f.propertyName}${n > 0 ? n : ''}`, `${f.operator}.${f.value}`]; + return [`epf_${f.propertyName}${n > 0 ? n : ''}`, `${f.dataType}.${f.operator}.${f.value}`]; }), ); } diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 8b9f143fc..baab7c2a4 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -2,7 +2,7 @@ import { PrismaClient } from '@/generated/prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; import { readReplicas } from '@prisma/extension-read-replicas'; import debug from 'debug'; -import { DEFAULT_PAGE_SIZE, FILTER_COLUMNS, OPERATORS, SESSION_COLUMNS } from './constants'; +import { DATA_TYPE, DEFAULT_PAGE_SIZE, FILTER_COLUMNS, OPERATORS, SESSION_COLUMNS } from './constants'; import { filtersObjectToArray } from './params'; import type { EventPropertyFilter, Operator, QueryFilters, QueryOptions } from './types'; @@ -44,6 +44,10 @@ const DATE_STRING_FORMATS = { second: 'YYYY-MM-DD"T"HH24:MI:SS', }; +function isUtcTimezone(timezone?: string) { + return timezone?.toLowerCase() === 'utc'; +} + function getAddIntervalQuery(field: string, interval: string): string { return `${field} + interval '${interval}'`; } @@ -57,7 +61,7 @@ function getCastColumnQuery(field: string, type: string): string { } function getDateSQL(field: string, unit: string, timezone?: string): string { - if (timezone && timezone !== 'utc') { + if (timezone && !isUtcTimezone(timezone)) { return `to_char(date_trunc('${unit}', ${field} at time zone '${timezone}'), '${DATE_FORMATS[unit]}')`; } @@ -65,7 +69,7 @@ function getDateSQL(field: string, unit: string, timezone?: string): string { } function getDateStringSQL(field: string, unit: keyof typeof DATE_STRING_FORMATS = 'utc', timezone?: string): string { - if (timezone && timezone !== 'utc') { + if (timezone && !isUtcTimezone(timezone)) { return `to_char(${field} at time zone '${timezone}', '${DATE_STRING_FORMATS[unit]}')`; } @@ -269,7 +273,10 @@ function parseFilters(filters: Record, options?: QueryOptions) { }; } -function getEventPropertyFilterQuery(filters: EventPropertyFilter[] = []): { +function getEventPropertyFilterQuery( + filters: EventPropertyFilter[] = [], + timezone?: string, +): { sql: string; params: Record; } { @@ -283,41 +290,81 @@ function getEventPropertyFilterQuery(filters: EventPropertyFilter[] = []): { const valParam = `epf_val_${i}`; params[keyParam] = propertyName; - const isNumeric = dataType === 2; - const col = isNumeric ? 'cast(number_value as decimal)' : 'string_value'; - let condition: string; - if (isNumeric) { - params[valParam] = parseFloat(value) || 0; - const opMap: Record = { - [OPERATORS.equals]: `${col} = {{${valParam}}}`, - [OPERATORS.notEquals]: `${col} != {{${valParam}}}`, - [OPERATORS.greaterThan]: `${col} > {{${valParam}}}`, - [OPERATORS.lessThan]: `${col} < {{${valParam}}}`, - [OPERATORS.greaterThanEquals]: `${col} >= {{${valParam}}}`, - [OPERATORS.lessThanEquals]: `${col} <= {{${valParam}}}`, - }; - condition = opMap[operator] ?? `${col} = {{${valParam}}}`; - } else if (EQUALITY_OPERATORS.includes(operator)) { - const vals = value.split(',').filter(Boolean); - if (!vals.length) return; - params[valParam] = vals; - condition = - operator === OPERATORS.equals - ? `${col} = ANY({{${valParam}::text[]}})` - : `${col} != ALL({{${valParam}::text[]}})`; - } else if (REGEX_OPERATORS.includes(operator)) { - if (!value) return; - params[valParam] = value; - condition = - operator === OPERATORS.regex ? `${col} ~* {{${valParam}}}` : `${col} !~* {{${valParam}}}`; - } else { - if (!value) return; - params[valParam] = `%${value}%`; - condition = - operator === OPERATORS.contains - ? `${col} ilike {{${valParam}}}` - : `${col} not ilike {{${valParam}}}`; + switch (dataType) { + case DATA_TYPE.number: { + const col = 'cast(number_value as decimal)'; + params[valParam] = parseFloat(value) || 0; + const opMap: Record = { + [OPERATORS.equals]: `${col} = {{${valParam}}}`, + [OPERATORS.notEquals]: `${col} != {{${valParam}}}`, + [OPERATORS.greaterThan]: `${col} > {{${valParam}}}`, + [OPERATORS.lessThan]: `${col} < {{${valParam}}}`, + [OPERATORS.greaterThanEquals]: `${col} >= {{${valParam}}}`, + [OPERATORS.lessThanEquals]: `${col} <= {{${valParam}}}`, + }; + condition = opMap[operator] ?? `${col} = {{${valParam}}}`; + break; + } + case DATA_TYPE.date: { + if (!value) return; + params[valParam] = value; + const dateCol = + timezone && !isUtcTimezone(timezone) + ? `(date_value at time zone {{timezone}})::date` + : `(date_value at time zone 'utc')::date`; + const opMap: Record = { + [OPERATORS.before]: `${dateCol} < {{${valParam}::date}}`, + [OPERATORS.after]: `${dateCol} > {{${valParam}::date}}`, + }; + condition = opMap[operator] ?? `${dateCol} = {{${valParam}::date}}`; + break; + } + case DATA_TYPE.array: { + if (!value) return; + params[valParam] = value; + condition = + operator === OPERATORS.contains + ? `exists ( + select 1 + from jsonb_array_elements_text(coalesce(string_value, '[]')::jsonb) as array_item(value) + where array_item.value = {{${valParam}}} + )` + : `not exists ( + select 1 + from jsonb_array_elements_text(coalesce(string_value, '[]')::jsonb) as array_item(value) + where array_item.value = {{${valParam}}} + )`; + break; + } + default: { + const col = 'string_value'; + + if (EQUALITY_OPERATORS.includes(operator)) { + const vals = value.split(',').filter(Boolean); + if (!vals.length) return; + params[valParam] = vals; + condition = + operator === OPERATORS.equals + ? `${col} = ANY({{${valParam}::text[]}})` + : `${col} != ALL({{${valParam}::text[]}})`; + } else if (REGEX_OPERATORS.includes(operator)) { + if (!value) return; + params[valParam] = value; + condition = + operator === OPERATORS.regex + ? `${col} ~* {{${valParam}}}` + : `${col} !~* {{${valParam}}}`; + } else { + if (!value) return; + params[valParam] = `%${value}%`; + condition = + operator === OPERATORS.contains + ? `${col} ilike {{${valParam}}}` + : `${col} not ilike {{${valParam}}}`; + } + break; + } } parts.push(`and website_event.event_id in ( diff --git a/src/queries/sql/events/getEventDataArraySeries.ts b/src/queries/sql/events/getEventDataArraySeries.ts index b9aa1e476..acf1e2034 100644 --- a/src/queries/sql/events/getEventDataArraySeries.ts +++ b/src/queries/sql/events/getEventDataArraySeries.ts @@ -33,8 +33,9 @@ async function relationalQuery( const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, websiteId, + timezone, }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters); + const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); return rawQuery( ` @@ -74,8 +75,8 @@ async function clickhouseQuery( ): Promise { const { timezone = 'UTC', unit = 'day' } = filters; const { rawQuery, getDateSQL, parseFilters, getEventPropertyFilterQuery } = clickhouse; - const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters); + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); return rawQuery( ` diff --git a/src/queries/sql/events/getEventDataDateSeries.ts b/src/queries/sql/events/getEventDataDateSeries.ts index eda146515..711968926 100644 --- a/src/queries/sql/events/getEventDataDateSeries.ts +++ b/src/queries/sql/events/getEventDataDateSeries.ts @@ -28,13 +28,14 @@ async function relationalQuery( filters: QueryFilters, eventFilters: EventPropertyFilter[] = [], ) : Promise { - const { timezone = 'UTC' } = filters; + const { timezone = 'utc' } = filters; const { rawQuery, parseFilters, getDateStringSQL, getEventPropertyFilterQuery } = prisma; const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, websiteId, + timezone, }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters); + const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); return rawQuery( ` @@ -72,8 +73,8 @@ async function clickhouseQuery( ): Promise { const { timezone = 'UTC' } = filters; const { rawQuery, parseFilters, getDateStringSQL, getEventPropertyFilterQuery } = clickhouse; - const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters); + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); return rawQuery( ` diff --git a/src/queries/sql/events/getEventDataEvents.ts b/src/queries/sql/events/getEventDataEvents.ts index f415e629d..313e8d97c 100644 --- a/src/queries/sql/events/getEventDataEvents.ts +++ b/src/queries/sql/events/getEventDataEvents.ts @@ -92,10 +92,10 @@ async function clickhouseQuery( return rawQuery( ` select - event_name as eventName, - data_key as propertyName, - data_type as dataType, - string_value as propertyValue, + event_data.event_name as eventName, + event_data.data_key as propertyName, + event_data.data_type as dataType, + event_data.string_value as propertyValue, count(*) as total from event_data any left join ( @@ -111,7 +111,11 @@ async function clickhouseQuery( where event_data.website_id = {websiteId:UUID} and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} ${filterQuery} - group by data_key, data_type, string_value, event_name + group by + event_data.data_key, + event_data.data_type, + event_data.string_value, + event_data.event_name order by 1 asc, 2 asc, 3 asc, 5 desc limit 500 `, @@ -123,9 +127,9 @@ async function clickhouseQuery( return rawQuery( ` select - event_name as eventName, - data_key as propertyName, - data_type as dataType, + event_data.event_name as eventName, + event_data.data_key as propertyName, + event_data.data_type as dataType, count(*) as total from event_data any left join ( @@ -141,7 +145,7 @@ async function clickhouseQuery( where event_data.website_id = {websiteId:UUID} and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} ${filterQuery} - group by data_key, data_type, event_name + group by event_data.data_key, event_data.data_type, event_data.event_name order by 1 asc, 2 asc limit 500 `, diff --git a/src/queries/sql/events/getEventDataNumericSeries.ts b/src/queries/sql/events/getEventDataNumericSeries.ts index aa039cdad..2624380ba 100644 --- a/src/queries/sql/events/getEventDataNumericSeries.ts +++ b/src/queries/sql/events/getEventDataNumericSeries.ts @@ -34,8 +34,9 @@ async function relationalQuery( const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, websiteId, + timezone, }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters); + const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); const aggSql = metric === 'avg' ? 'avg(cast(event_data.number_value as decimal))' : metric === 'count' ? 'count(*)' : @@ -78,8 +79,8 @@ async function clickhouseQuery( ): Promise<{ t: string; y: number }[]> { const { timezone = 'UTC', unit = 'day' } = filters; const { rawQuery, getDateSQL, parseFilters, getEventPropertyFilterQuery } = clickhouse; - const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters); + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); const aggSql = metric === 'avg' ? 'avg(event_data.number_value)' : metric === 'count' ? 'count()' : diff --git a/src/queries/sql/events/getEventDataNumericStats.ts b/src/queries/sql/events/getEventDataNumericStats.ts index 8b649b792..64a3f884b 100644 --- a/src/queries/sql/events/getEventDataNumericStats.ts +++ b/src/queries/sql/events/getEventDataNumericStats.ts @@ -27,12 +27,14 @@ async function relationalQuery( filters: QueryFilters, eventFilters: EventPropertyFilter[] = [], ) { + const { timezone = 'utc' } = filters; const { rawQuery, parseFilters, getEventPropertyFilterQuery } = prisma; const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, websiteId, + timezone, }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters); + const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); return rawQuery( ` @@ -69,9 +71,10 @@ async function clickhouseQuery( filters: QueryFilters, eventFilters: EventPropertyFilter[] = [], ): Promise { + const { timezone = 'UTC' } = filters; const { rawQuery, parseFilters, getEventPropertyFilterQuery } = clickhouse; - const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters); + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); return rawQuery( ` diff --git a/src/queries/sql/events/getEventDataPivot.ts b/src/queries/sql/events/getEventDataPivot.ts index 00044b649..18f8aabda 100644 --- a/src/queries/sql/events/getEventDataPivot.ts +++ b/src/queries/sql/events/getEventDataPivot.ts @@ -21,6 +21,7 @@ export async function getEventDataPivot( } async function relationalQuery(websiteId: string, eventName: string, filters: QueryFilters, eventFilters: EventPropertyFilter[] = []) { + const { timezone = 'utc' } = filters; const { rawQuery, parseFilters, getEventPropertyFilterQuery } = prisma; const { page = 1, pageSize } = filters; const size = +pageSize || DEFAULT_PAGE_SIZE; @@ -29,8 +30,9 @@ async function relationalQuery(websiteId: string, eventName: string, filters: Qu const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, websiteId, + timezone, }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters); + const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); const countResult = await rawQuery( ` @@ -81,7 +83,9 @@ async function relationalQuery(websiteId: string, eventName: string, filters: Qu coalesce( case when event_data.data_type = 1 then event_data.string_value end, case when event_data.data_type = 2 then cast(event_data.number_value as varchar) end, + case when event_data.data_type = 3 then event_data.string_value end, case when event_data.data_type = 4 then cast(event_data.date_value as varchar) end, + case when event_data.data_type = 5 then event_data.string_value end, '' ) as "value", event_data.data_type as "dataType" @@ -115,13 +119,14 @@ async function relationalQuery(websiteId: string, eventName: string, filters: Qu } async function clickhouseQuery(websiteId: string, eventName: string, filters: QueryFilters, eventFilters: EventPropertyFilter[] = []) { + const { timezone = 'UTC' } = filters; const { rawQuery, parseFilters, getEventPropertyFilterQuery } = clickhouse; const { page = 1, pageSize } = filters; const size = +pageSize || DEFAULT_PAGE_SIZE; const offset = +size * (+page - 1); - const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters); + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); const count = await rawQuery( ` @@ -149,11 +154,11 @@ async function clickhouseQuery(websiteId: string, eventName: string, filters: Qu const data = await rawQuery( ` select - event_id as eventId, - session_id as sessionId, - event_name as eventName, - url_path as urlPath, - created_at as createdAt, + event_data_pivot.event_id as eventId, + event_data_pivot.session_id as sessionId, + event_data_pivot.event_name as eventName, + event_data_pivot.url_path as urlPath, + event_data_pivot.created_at as createdAt, groupArrayMerge(property_keys) as propertyKeys, groupArrayMerge(property_values) as propertyValues from umami.event_data_pivot @@ -172,8 +177,13 @@ async function clickhouseQuery(websiteId: string, eventName: string, filters: Qu and event_data_pivot.created_at between {startDate:DateTime64} and {endDate:DateTime64} ${filterQuery} ${epfSQL} - group by event_id, session_id, event_name, url_path, created_at - order by created_at desc + group by + event_data_pivot.event_id, + event_data_pivot.session_id, + event_data_pivot.event_name, + event_data_pivot.url_path, + event_data_pivot.created_at + order by event_data_pivot.created_at desc limit ${size} offset ${offset} `, { ...queryParams, eventName, ...epfParams }, diff --git a/src/queries/sql/events/getEventDataProperties.ts b/src/queries/sql/events/getEventDataProperties.ts index b7b7c7379..943dca84d 100644 --- a/src/queries/sql/events/getEventDataProperties.ts +++ b/src/queries/sql/events/getEventDataProperties.ts @@ -66,9 +66,9 @@ async function clickhouseQuery( return rawQuery( ` select - event_name as eventName, - data_key as propertyName, - data_type as dataType, + event_data.event_name as eventName, + event_data.data_key as propertyName, + event_data.data_type as dataType, count(*) as total from event_data any left join ( @@ -84,7 +84,7 @@ async function clickhouseQuery( where event_data.website_id = {websiteId:UUID} and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} ${filterQuery} - group by event_name, data_key, data_type + group by event_data.event_name, event_data.data_key, event_data.data_type order by 1, 4 desc limit 500 `, diff --git a/src/queries/sql/events/getEventDataPropertySeries.ts b/src/queries/sql/events/getEventDataPropertySeries.ts index e75004fa0..e48045236 100644 --- a/src/queries/sql/events/getEventDataPropertySeries.ts +++ b/src/queries/sql/events/getEventDataPropertySeries.ts @@ -33,8 +33,9 @@ async function relationalQuery( const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, websiteId, + timezone, }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters); + const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); return rawQuery( ` @@ -73,8 +74,8 @@ async function clickhouseQuery( ): Promise { const { timezone = 'UTC', unit = 'day' } = filters; const { rawQuery, getDateSQL, parseFilters, getEventPropertyFilterQuery } = clickhouse; - const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters); + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); return rawQuery( ` diff --git a/src/queries/sql/events/getEventDataValues.ts b/src/queries/sql/events/getEventDataValues.ts index f644934e3..5063646b7 100644 --- a/src/queries/sql/events/getEventDataValues.ts +++ b/src/queries/sql/events/getEventDataValues.ts @@ -1,4 +1,5 @@ import clickhouse from '@/lib/clickhouse'; +import { DATA_TYPE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { QueryFilters } from '@/lib/types'; @@ -11,7 +12,7 @@ interface WebsiteEventData { } export async function getEventDataValues( - ...args: [websiteId: string, filters: QueryFilters & { propertyName?: string }] + ...args: [websiteId: string, filters: QueryFilters & { propertyName?: string; dataType?: number }] ): Promise { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -21,14 +22,42 @@ export async function getEventDataValues( async function relationalQuery( websiteId: string, - filters: QueryFilters & { propertyName?: string }, + filters: QueryFilters & { propertyName?: string; dataType?: number }, ) { const { rawQuery, parseFilters, getDateSQL } = prisma; + const { dataType } = filters; const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, }); + if (dataType === DATA_TYPE.array) { + return rawQuery( + ` + select + array_item.value as "value", + count(*) as "total" + from event_data + join website_event on website_event.event_id = event_data.website_event_id + and website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + cross join lateral jsonb_array_elements_text(coalesce(event_data.string_value, '[]')::jsonb) as array_item(value) + ${cohortQuery} + ${joinSessionQuery} + where event_data.website_id = {{websiteId::uuid}} + and event_data.created_at between {{startDate}} and {{endDate}} + and event_data.data_key = {{propertyName}} + and event_data.data_type = ${DATA_TYPE.array} + ${filterQuery} + group by array_item.value + order by 2 desc + limit 100 + `, + queryParams, + FUNCTION_NAME, + ); + } + return rawQuery( ` select @@ -59,11 +88,43 @@ async function relationalQuery( async function clickhouseQuery( websiteId: string, - filters: QueryFilters & { propertyName?: string }, + filters: QueryFilters & { propertyName?: string; dataType?: number }, ): Promise<{ value: string; total: number }[]> { const { rawQuery, parseFilters } = clickhouse; + const { dataType } = filters; const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId }); + if (dataType === DATA_TYPE.array) { + return rawQuery( + ` + select + arrayJoin(JSONExtract(ifNull(event_data.string_value, '[]'), 'Array(String)')) as "value", + count(*) as "total" + from event_data + any left join ( + select * + from website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_type = 2) website_event + on website_event.event_id = event_data.event_id + and website_event.session_id = event_data.session_id + and website_event.website_id = event_data.website_id + ${cohortQuery} + where event_data.website_id = {websiteId:UUID} + and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_data.data_key = {propertyName:String} + and event_data.data_type = ${DATA_TYPE.array} + ${filterQuery} + group by value + order by 2 desc + limit 100 + `, + queryParams, + FUNCTION_NAME, + ); + } + return rawQuery( ` select From 3ebd5a44e837c8492f5912d0019ab86da54e059e Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 28 Apr 2026 19:47:38 -0700 Subject: [PATCH 51/59] Fix 500 on /api/send when client IP is malformed. --- src/lib/detect.ts | 8 +++++--- src/lib/ip.ts | 4 +--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/detect.ts b/src/lib/detect.ts index 2e78a1f35..643ed01a2 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -77,8 +77,10 @@ function decodeHeader(s: string | undefined | null): string | undefined | null { } export async function getLocation(ip: string = '', headers: Headers, skipHeaders: boolean) { - // Ignore local ips - if (!ip || (await isLocalhost(ip))) { + const cleanIp = stripPort(ip); + + // Ignore local or invalid ips + if (!cleanIp || !ipaddr.isValid(cleanIp) || (await isLocalhost(cleanIp))) { return null; } @@ -108,7 +110,7 @@ export async function getLocation(ip: string = '', headers: Headers, skipHeaders ); } - const result = globalThis[MAXMIND]?.get(stripPort(ip)); + const result = globalThis[MAXMIND]?.get(cleanIp); if (result) { const country = result.country?.iso_code ?? result?.registered_country?.iso_code; diff --git a/src/lib/ip.ts b/src/lib/ip.ts index a78c7e56e..08e77c65e 100644 --- a/src/lib/ip.ts +++ b/src/lib/ip.ts @@ -84,9 +84,7 @@ export function getIpAddress(headers: Headers) { if (header === 'forwarded') { const match = ip.match(/for=(\[?[0-9a-fA-F:.]+]?)/); - if (match) { - return resolveIp(match[1]); - } + return match ? resolveIp(match[1]) : undefined; } return resolveIp(ip); From 680a1ca1a29da9d9cbc2be6f1a4d31da3a1addda Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 29 Apr 2026 13:11:03 -0700 Subject: [PATCH 52/59] refactor Property components to be resused for both session/event data. Implement session-data screens + filtering. Remove all event-data specific compoenents/queries --- .../migrations/11_add_event_data_pivot.sql | 56 ---- .../11_add_event_session_data_pivot.sql | 110 +++++++ db/clickhouse/schema.sql | 36 +++ .../event-data/EventDataFilterButton.tsx | 34 --- .../event-data/EventDataPivotTable.tsx | 32 +- .../[websiteId]/events/EventProperties.tsx | 53 ++-- .../HighCardinalityPropertyChart.tsx | 133 +++++++++ .../session-data/SessionDataPivotTable.tsx | 143 +++++++++ .../SessionStringPropertyView.tsx | 53 ++++ .../sessions/SessionProperties.tsx | 218 +++++++++----- .../[websiteId]/sessions/SessionsPage.tsx | 12 +- .../[websiteId]/session-data-pivot/route.ts | 39 +++ .../session-data/array-series/route.ts | 40 +++ .../session-data/date-series/route.ts | 39 +++ .../session-data/numeric-series/route.ts | 41 +++ .../session-data/numeric-stats/route.ts | 39 +++ .../session-data/properties/route.ts | 13 +- .../session-data/property-series/route.ts | 40 +++ .../[websiteId]/session-data/stats/route.ts | 40 +++ .../[websiteId]/session-data/values/route.ts | 4 +- src/components/hooks/index.ts | 17 +- .../queries/useEventDataArraySeriesQuery.ts | 47 --- .../queries/useEventDataDateSeriesQuery.ts | 45 --- .../hooks/queries/useEventDataEventsQuery.ts | 27 -- .../hooks/queries/useEventDataFieldsQuery.ts | 32 -- .../queries/useEventDataNumericSeriesQuery.ts | 39 --- .../queries/useEventDataNumericStatsQuery.ts | 36 --- .../queries/useEventDataPropertiesQuery.ts | 2 +- .../useEventDataPropertySeriesQuery.ts | 37 --- .../hooks/queries/useEventStatsQuery.ts | 2 +- .../queries/usePropertyArraySeriesQuery.ts | 44 +++ .../queries/usePropertyDateSeriesQuery.ts | 43 +++ .../hooks/queries/usePropertyFieldsQuery.ts | 40 +++ .../queries/usePropertyNumericSeriesQuery.ts | 46 +++ .../queries/usePropertyNumericStatsQuery.ts | 43 +++ .../hooks/queries/usePropertySeriesQuery.ts | 44 +++ .../hooks/queries/usePropertyValuesQuery.ts | 46 +++ .../hooks/queries/useResultQuery.ts | 2 +- ...esQuery.ts => useSessionDataPivotQuery.ts} | 23 +- .../queries/useSessionDataPropertiesQuery.ts | 27 +- .../hooks/queries/useSessionDataStatsQuery.ts | 35 +++ src/components/hooks/useFilterParameters.ts | 17 +- .../property-data/PropertyChart.tsx} | 60 ++-- .../property-data/PropertyDateChart.tsx} | 276 ++++++------------ .../property-data/PropertyFilterBar.tsx} | 16 +- .../property-data/PropertyFilterButton.tsx | 41 +++ .../property-data/PropertyFilterEditForm.tsx} | 57 ++-- .../property-data/PropertyFilterRecord.tsx} | 50 ++-- .../property-data/PropertyNumericChart.tsx} | 84 ++---- src/lib/clickhouse.ts | 22 +- src/lib/params.ts | 35 ++- src/lib/prisma.ts | 27 +- src/lib/request.ts | 2 +- src/lib/types.ts | 29 +- .../sql/events/getEventDataArraySeries.ts | 16 +- .../sql/events/getEventDataDateSeries.ts | 16 +- .../sql/events/getEventDataNumericSeries.ts | 16 +- .../sql/events/getEventDataNumericStats.ts | 16 +- src/queries/sql/events/getEventDataPivot.ts | 24 +- .../sql/events/getEventDataPropertySeries.ts | 16 +- src/queries/sql/events/getEventDataValues.ts | 2 + src/queries/sql/index.ts | 7 + .../sql/sessions/getSessionDataArraySeries.ts | 99 +++++++ .../sql/sessions/getSessionDataDateSeries.ts | 96 ++++++ .../sessions/getSessionDataNumericSeries.ts | 119 ++++++++ .../sessions/getSessionDataNumericStats.ts | 106 +++++++ .../sql/sessions/getSessionDataPivot.ts | 215 ++++++++++++++ .../sql/sessions/getSessionDataProperties.ts | 110 +++++-- .../sessions/getSessionDataPropertySeries.ts | 98 +++++++ .../sql/sessions/getSessionDataStats.ts | 185 ++++++++++++ .../sql/sessions/getSessionDataValues.ts | 63 +++- 71 files changed, 2814 insertions(+), 958 deletions(-) delete mode 100644 db/clickhouse/migrations/11_add_event_data_pivot.sql create mode 100644 db/clickhouse/migrations/11_add_event_session_data_pivot.sql delete mode 100644 src/app/(main)/websites/[websiteId]/event-data/EventDataFilterButton.tsx create mode 100644 src/app/(main)/websites/[websiteId]/session-data/HighCardinalityPropertyChart.tsx create mode 100644 src/app/(main)/websites/[websiteId]/session-data/SessionDataPivotTable.tsx create mode 100644 src/app/(main)/websites/[websiteId]/session-data/SessionStringPropertyView.tsx create mode 100644 src/app/api/websites/[websiteId]/session-data-pivot/route.ts create mode 100644 src/app/api/websites/[websiteId]/session-data/array-series/route.ts create mode 100644 src/app/api/websites/[websiteId]/session-data/date-series/route.ts create mode 100644 src/app/api/websites/[websiteId]/session-data/numeric-series/route.ts create mode 100644 src/app/api/websites/[websiteId]/session-data/numeric-stats/route.ts create mode 100644 src/app/api/websites/[websiteId]/session-data/property-series/route.ts create mode 100644 src/app/api/websites/[websiteId]/session-data/stats/route.ts delete mode 100644 src/components/hooks/queries/useEventDataArraySeriesQuery.ts delete mode 100644 src/components/hooks/queries/useEventDataDateSeriesQuery.ts delete mode 100644 src/components/hooks/queries/useEventDataEventsQuery.ts delete mode 100644 src/components/hooks/queries/useEventDataFieldsQuery.ts delete mode 100644 src/components/hooks/queries/useEventDataNumericSeriesQuery.ts delete mode 100644 src/components/hooks/queries/useEventDataNumericStatsQuery.ts delete mode 100644 src/components/hooks/queries/useEventDataPropertySeriesQuery.ts create mode 100644 src/components/hooks/queries/usePropertyArraySeriesQuery.ts create mode 100644 src/components/hooks/queries/usePropertyDateSeriesQuery.ts create mode 100644 src/components/hooks/queries/usePropertyFieldsQuery.ts create mode 100644 src/components/hooks/queries/usePropertyNumericSeriesQuery.ts create mode 100644 src/components/hooks/queries/usePropertyNumericStatsQuery.ts create mode 100644 src/components/hooks/queries/usePropertySeriesQuery.ts create mode 100644 src/components/hooks/queries/usePropertyValuesQuery.ts rename src/components/hooks/queries/{useEventDataValuesQuery.ts => useSessionDataPivotQuery.ts} (50%) create mode 100644 src/components/hooks/queries/useSessionDataStatsQuery.ts rename src/{app/(main)/websites/[websiteId]/event-data/EventDataPropertyChart.tsx => components/property-data/PropertyChart.tsx} (84%) rename src/{app/(main)/websites/[websiteId]/event-data/EventDataDateChart.tsx => components/property-data/PropertyDateChart.tsx} (51%) rename src/{app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx => components/property-data/PropertyFilterBar.tsx} (90%) create mode 100644 src/components/property-data/PropertyFilterButton.tsx rename src/{app/(main)/websites/[websiteId]/event-data/EventDataFilterEditForm.tsx => components/property-data/PropertyFilterEditForm.tsx} (67%) rename src/{app/(main)/websites/[websiteId]/event-data/EventDataFilterRecord.tsx => components/property-data/PropertyFilterRecord.tsx} (89%) rename src/{app/(main)/websites/[websiteId]/event-data/EventDataNumericChart.tsx => components/property-data/PropertyNumericChart.tsx} (65%) create mode 100644 src/queries/sql/sessions/getSessionDataArraySeries.ts create mode 100644 src/queries/sql/sessions/getSessionDataDateSeries.ts create mode 100644 src/queries/sql/sessions/getSessionDataNumericSeries.ts create mode 100644 src/queries/sql/sessions/getSessionDataNumericStats.ts create mode 100644 src/queries/sql/sessions/getSessionDataPivot.ts create mode 100644 src/queries/sql/sessions/getSessionDataPropertySeries.ts create mode 100644 src/queries/sql/sessions/getSessionDataStats.ts diff --git a/db/clickhouse/migrations/11_add_event_data_pivot.sql b/db/clickhouse/migrations/11_add_event_data_pivot.sql deleted file mode 100644 index 15ba1a7e8..000000000 --- a/db/clickhouse/migrations/11_add_event_data_pivot.sql +++ /dev/null @@ -1,56 +0,0 @@ -CREATE TABLE IF NOT EXISTS umami.event_data_pivot -( - website_id UUID, - session_id UUID, - event_id UUID, - event_name LowCardinality(String), - url_path String, - created_at DateTime('UTC'), - property_keys AggregateFunction(groupArray, String), - property_values AggregateFunction(groupArray, String), - property_types AggregateFunction(groupArray, UInt32) -) -ENGINE = AggregatingMergeTree() -PARTITION BY toYYYYMM(created_at) -ORDER BY (website_id, event_name, created_at, event_id) -SETTINGS index_granularity = 8192; - -CREATE MATERIALIZED VIEW IF NOT EXISTS umami.event_data_pivot_mv -TO umami.event_data_pivot -AS SELECT - website_id, - session_id, - event_id, - event_name, - url_path, - created_at, - groupArrayState(data_key) AS property_keys, - groupArrayState(multiIf( - data_type IN (1, 3, 5), ifNull(string_value, ''), - data_type = 2, toString(ifNull(number_value, 0)), - data_type = 4, toString(ifNull(date_value, toDateTime(0))), - '' - )) AS property_values, - groupArrayState(data_type) AS property_types -FROM umami.event_data -GROUP BY website_id, session_id, event_id, event_name, url_path, created_at; - --- Backfill existing data -INSERT INTO umami.event_data_pivot -SELECT - website_id, - session_id, - event_id, - event_name, - url_path, - created_at, - groupArrayState(data_key), - groupArrayState(multiIf( - data_type IN (1, 3, 5), ifNull(string_value, ''), - data_type = 2, toString(ifNull(number_value, 0)), - data_type = 4, toString(ifNull(date_value, toDateTime(0))), - '' - )), - groupArrayState(data_type) -FROM umami.event_data -GROUP BY website_id, session_id, event_id, event_name, url_path, created_at; diff --git a/db/clickhouse/migrations/11_add_event_session_data_pivot.sql b/db/clickhouse/migrations/11_add_event_session_data_pivot.sql new file mode 100644 index 000000000..2d44cf0bf --- /dev/null +++ b/db/clickhouse/migrations/11_add_event_session_data_pivot.sql @@ -0,0 +1,110 @@ +CREATE TABLE IF NOT EXISTS umami.event_data_pivot +( + website_id UUID, + session_id UUID, + event_id UUID, + event_name LowCardinality(String), + url_path String, + created_at DateTime('UTC'), + property_keys AggregateFunction(groupArray, String), + property_values AggregateFunction(groupArray, String), + property_types AggregateFunction(groupArray, UInt32) +) +ENGINE = AggregatingMergeTree() +PARTITION BY toYYYYMM(created_at) +ORDER BY (website_id, event_name, created_at, event_id) +SETTINGS index_granularity = 8192; + +CREATE MATERIALIZED VIEW IF NOT EXISTS umami.event_data_pivot_mv +TO umami.event_data_pivot +AS SELECT + website_id, + session_id, + event_id, + event_name, + url_path, + created_at, + groupArrayState(data_key) AS property_keys, + groupArrayState(multiIf( + data_type IN (1, 3, 5), ifNull(string_value, ''), + data_type = 2, toString(ifNull(number_value, 0)), + data_type = 4, toString(ifNull(date_value, toDateTime(0))), + '' + )) AS property_values, + groupArrayState(data_type) AS property_types +FROM umami.event_data +GROUP BY website_id, session_id, event_id, event_name, url_path, created_at; + +-- Backfill existing event data +INSERT INTO umami.event_data_pivot +SELECT + website_id, + session_id, + event_id, + event_name, + url_path, + created_at, + groupArrayState(data_key), + groupArrayState(multiIf( + data_type IN (1, 3, 5), ifNull(string_value, ''), + data_type = 2, toString(ifNull(number_value, 0)), + data_type = 4, toString(ifNull(date_value, toDateTime(0))), + '' + )), + groupArrayState(data_type) +FROM umami.event_data +GROUP BY website_id, session_id, event_id, event_name, url_path, created_at; + +CREATE TABLE IF NOT EXISTS umami.session_data_pivot +( + website_id UUID, + session_id UUID, + distinct_id String, + created_year_month UInt32, + created_at AggregateFunction(max, DateTime('UTC')), + property_keys AggregateFunction(groupArray, String), + property_values AggregateFunction(groupArray, String), + property_types AggregateFunction(groupArray, UInt32) +) +ENGINE = AggregatingMergeTree() +PARTITION BY created_year_month +ORDER BY (website_id, session_id, distinct_id) +SETTINGS index_granularity = 8192; + +CREATE MATERIALIZED VIEW IF NOT EXISTS umami.session_data_pivot_mv +TO umami.session_data_pivot +AS SELECT + website_id, + session_id, + ifNull(distinct_id, '') AS distinct_id, + toYYYYMM(max(session_data.created_at)) AS created_year_month, + maxState(session_data.created_at) AS created_at, + groupArrayState(data_key) AS property_keys, + groupArrayState(multiIf( + data_type IN (1, 3, 5), ifNull(string_value, ''), + data_type = 2, toString(ifNull(number_value, 0)), + data_type = 4, toString(ifNull(date_value, toDateTime(0))), + '' + )) AS property_values, + groupArrayState(data_type) AS property_types +FROM umami.session_data +GROUP BY website_id, session_id, distinct_id; + +-- Backfill existing session data +INSERT INTO umami.session_data_pivot +SELECT + website_id, + session_id, + ifNull(distinct_id, '') AS distinct_id, + toYYYYMM(max(session_data.created_at)) AS created_year_month, + maxState(session_data.created_at) AS created_at, + groupArrayState(data_key), + groupArrayState(multiIf( + data_type IN (1, 3, 5), ifNull(string_value, ''), + data_type = 2, toString(ifNull(number_value, 0)), + data_type = 4, toString(ifNull(date_value, toDateTime(0))), + '' + )), + groupArrayState(data_type) +FROM umami.session_data +GROUP BY website_id, session_id, distinct_id; diff --git a/db/clickhouse/schema.sql b/db/clickhouse/schema.sql index 0540c97b2..c5c8dce67 100644 --- a/db/clickhouse/schema.sql +++ b/db/clickhouse/schema.sql @@ -344,3 +344,39 @@ AS SELECT groupArrayState(data_type) AS property_types FROM umami.event_data GROUP BY website_id, session_id, event_id, event_name, url_path, created_at; + +-- Create session_data_pivot +CREATE TABLE IF NOT EXISTS umami.session_data_pivot +( + website_id UUID, + session_id UUID, + distinct_id String, + created_year_month UInt32, + created_at AggregateFunction(max, DateTime('UTC')), + property_keys AggregateFunction(groupArray, String), + property_values AggregateFunction(groupArray, String), + property_types AggregateFunction(groupArray, UInt32) +) +ENGINE = AggregatingMergeTree() +PARTITION BY created_year_month +ORDER BY (website_id, session_id, distinct_id) +SETTINGS index_granularity = 8192; + +CREATE MATERIALIZED VIEW IF NOT EXISTS umami.session_data_pivot_mv +TO umami.session_data_pivot +AS SELECT + website_id, + session_id, + ifNull(distinct_id, '') AS distinct_id, + toYYYYMM(max(session_data.created_at)) AS created_year_month, + maxState(session_data.created_at) AS created_at, + groupArrayState(data_key) AS property_keys, + groupArrayState(multiIf( + data_type IN (1, 3, 5), ifNull(string_value, ''), + data_type = 2, toString(ifNull(number_value, 0)), + data_type = 4, toString(ifNull(date_value, toDateTime(0))), + '' + )) AS property_values, + groupArrayState(data_type) AS property_types +FROM umami.session_data +GROUP BY website_id, session_id, distinct_id; diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterButton.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterButton.tsx deleted file mode 100644 index 4f39b41e2..000000000 --- a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterButton.tsx +++ /dev/null @@ -1,34 +0,0 @@ -'use client'; -import { useMessages } from '@/components/hooks'; -import { ListFilter } from '@/components/icons'; -import { DialogButton } from '@/components/input/DialogButton'; -import type { EventPropertyFilter } from '@/lib/types'; -import { EventDataFilterEditForm } from './EventDataFilterEditForm'; - -export function EventDataFilterButton({ - websiteId, - eventName, - eventFilters, - onApply, -}: { - websiteId: string; - eventName: string; - eventFilters: EventPropertyFilter[]; - onApply: (filters: EventPropertyFilter[]) => void; -}) { - const { t, labels } = useMessages(); - - return ( - } label={t(labels.propertyFilter)} variant="outline"> - {({ close }) => ( - - )} - - ); -} diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataPivotTable.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataPivotTable.tsx index fa91e56bd..900d5b20e 100644 --- a/src/app/(main)/websites/[websiteId]/event-data/EventDataPivotTable.tsx +++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataPivotTable.tsx @@ -70,6 +70,14 @@ export function EventDataPivotTable({ router.push(updateParams({ page })); }; + const renderTruncatedText = (value: string) => ( + + + {value} + + + ); + return ( - {(row: any) => ( - - {row.urlPath} - - )} + {(row: any) => renderTruncatedText(row.urlPath ?? '')} {propertyKeys.map(key => ( - {(row: any) => ( - - {row[key] ?? ''} - - )} + {(row: any) => renderTruncatedText(row[key] ?? '')} ))} @@ -125,19 +125,11 @@ export function EventDataPivotTable({ )} - {(row: any) => ( - - {row.urlPath} - - )} + {(row: any) => renderTruncatedText(row.urlPath ?? '')} {propertyKeys.map(key => ( - {(row: any) => ( - - {row[key] ?? ''} - - )} + {(row: any) => renderTruncatedText(row[key] ?? '')} ))} diff --git a/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx b/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx index 6764f4ebf..b7b31b9c7 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx @@ -5,18 +5,18 @@ import { LoadingPanel } from '@/components/common/LoadingPanel'; import { Panel } from '@/components/common/Panel'; import { useEventDataPropertiesQuery, useMessages } from '@/components/hooks'; import { DATA_TYPE } from '@/lib/constants'; -import type { EventPropertyFilter } from '@/lib/types'; -import { EventDataDateChart } from '../event-data/EventDataDateChart'; -import { EventDataFilterBar } from '../event-data/EventDataFilterBar'; -import { EventDataFilterButton } from '../event-data/EventDataFilterButton'; -import { EventDataNumericChart } from '../event-data/EventDataNumericChart'; +import type { PropertyFilter } from '@/lib/types'; +import { PropertyChart } from '@/components/property-data/PropertyChart'; +import { PropertyDateChart } from '@/components/property-data/PropertyDateChart'; +import { PropertyFilterBar } from '@/components/property-data/PropertyFilterBar'; +import { PropertyFilterButton } from '@/components/property-data/PropertyFilterButton'; +import { PropertyNumericChart } from '@/components/property-data/PropertyNumericChart'; import { EventDataPivotTable } from '../event-data/EventDataPivotTable'; -import { EventDataPropertyChart } from '../event-data/EventDataPropertyChart'; export function EventProperties({ websiteId }: { websiteId: string }) { const [eventName, setEventName] = useState(''); const [propertyName, setPropertyName] = useState(''); - const [eventFilters, setEventFilters] = useState([]); + const [propertyFilters, setPropertyFilters] = useState([]); const { t, labels } = useMessages(); const { data, isLoading, isFetching, error } = useEventDataPropertiesQuery(websiteId); @@ -34,10 +34,8 @@ export function EventProperties({ websiteId }: { websiteId: string }) { return data .filter((field: { eventName: string }) => field.eventName === eventName) .filter((field: { propertyName: string; dataType: number }) => { - const key = `${field.propertyName}:${field.dataType}`; - - if (seen.has(key)) return false; - seen.add(key); + if (seen.has(field.propertyName)) return false; + seen.add(field.propertyName); return true; }); @@ -52,7 +50,7 @@ export function EventProperties({ websiteId }: { websiteId: string }) { const handleEventChange = (value: string) => { setEventName(value); setPropertyName(''); - setEventFilters([]); + setPropertyFilters([]); }; return ( @@ -117,52 +115,57 @@ export function EventProperties({ websiteId }: { websiteId: string }) { marginTop={{ base: '2', md: '0' }} style={{ minWidth: 0 }} > - )} )} {eventName && ( - + )} {eventName && propertyName && ( {(selectedProperty?.dataType === DATA_TYPE.string || selectedProperty?.dataType === DATA_TYPE.boolean) && ( - )} {selectedProperty?.dataType === DATA_TYPE.number && ( - )} {selectedProperty?.dataType === DATA_TYPE.date && ( - )} {selectedProperty?.dataType === DATA_TYPE.array && ( - )} @@ -173,7 +176,7 @@ export function EventProperties({ websiteId }: { websiteId: string }) { )} diff --git a/src/app/(main)/websites/[websiteId]/session-data/HighCardinalityPropertyChart.tsx b/src/app/(main)/websites/[websiteId]/session-data/HighCardinalityPropertyChart.tsx new file mode 100644 index 000000000..1dd39d757 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/session-data/HighCardinalityPropertyChart.tsx @@ -0,0 +1,133 @@ +'use client'; +import { PieChart } from '@/components/charts/PieChart'; +import { Empty } from '@/components/common/Empty'; +import { useMessages, useMobile } from '@/components/hooks'; +import { ListTable } from '@/components/metrics/ListTable'; +import { CHART_COLORS } from '@/lib/constants'; +import { formatLongNumber, formatShortTime } from '@/lib/format'; +import type { PropertyLeaderboardRow } from '@/lib/types'; +import { Column, DataColumn, DataTable, Grid, Row, Text } from '@umami/react-zen'; +import { useMemo } from 'react'; + +export function HighCardinalityPropertyChart({ rows }: { rows: PropertyLeaderboardRow[] }) { + const { t, labels } = useMessages(); + const { isPhone } = useMobile(); + const activeTable = useMemo(() => rows.filter(row => row.activity > 0).slice(0, 10), [rows]); + const userMix = useMemo(() => { + const totalNew = rows.reduce((sum, row) => sum + row.newSessions, 0); + const totalReturning = rows.reduce((sum, row) => sum + row.returningSessions, 0); + const total = totalNew + totalReturning; + + return { + newCount: totalNew, + returningCount: totalReturning, + newRate: total ? (totalNew / total) * 100 : 0, + returningRate: total ? (totalReturning / total) * 100 : 0, + }; + }, [rows]); + const sessionTypeTable = useMemo(() => { + return [ + { + label: 'New', + count: userMix.newCount, + percent: userMix.newRate, + }, + { + label: 'Returning', + count: userMix.returningCount, + percent: userMix.returningRate, + }, + ].filter(row => row.count > 0); + }, [userMix]); + const sessionTypeChartData = useMemo(() => { + if (!sessionTypeTable.length) return null; + + return { + labels: sessionTypeTable.map(row => row.label), + datasets: [ + { + data: sessionTypeTable.map(row => row.count), + backgroundColor: [CHART_COLORS[0], CHART_COLORS[1]], + borderWidth: 0, + }, + ], + }; + }, [sessionTypeTable]); + + return ( + + + + + {activeTable.length === 0 ? ( + + ) : isPhone ? ( + + + + {row => ( + + + {row.label} + + + )} + + + + ) : ( + + + + + {row => ( + + + {row.label} + + + )} + + + {row => formatLongNumber(row.sessions)} + + + {row => formatLongNumber(row.visits)} + + + {row => formatLongNumber(row.views)} + + + {row => formatLongNumber(row.events)} + + + {row => + formatShortTime(Math.abs(~~Number(row.totaltime)), ['h', 'm', 's'], ' ') + } + + + + + )} + + + + + + + {sessionTypeChartData && } + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/session-data/SessionDataPivotTable.tsx b/src/app/(main)/websites/[websiteId]/session-data/SessionDataPivotTable.tsx new file mode 100644 index 000000000..c7c3716bc --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/session-data/SessionDataPivotTable.tsx @@ -0,0 +1,143 @@ +'use client'; +import { Column, DataColumn, DataTable, Row, Text } from '@umami/react-zen'; +import { useMemo } from 'react'; +import { Avatar } from '@/components/common/Avatar'; +import { DateDistance } from '@/components/common/DateDistance'; +import { Empty } from '@/components/common/Empty'; +import Link from '@/components/common/Link'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { Pager } from '@/components/common/Pager'; +import { + useMessages, + useMobile, + useNavigation, + useSessionDataPivotQuery, +} from '@/components/hooks'; +import type { PropertyFilter } from '@/lib/types'; + +export function SessionDataPivotTable({ + websiteId, + propertyName, + propertyKeys: providedPropertyKeys, + propertyFilters = [], +}: { + websiteId: string; + propertyName: string; + propertyKeys?: string[]; + propertyFilters?: PropertyFilter[]; +}) { + const { t, labels } = useMessages(); + const { router, updateParams } = useNavigation(); + const { isMobile } = useMobile(); + + const pivotQuery = useSessionDataPivotQuery(websiteId, propertyName, propertyFilters); + + const propertyKeys = useMemo(() => { + if (providedPropertyKeys) return providedPropertyKeys; + return []; + }, [providedPropertyKeys]); + + const tableMinWidth = useMemo(() => `${72 + 220 + 180 + propertyKeys.length * 160}px`, [propertyKeys.length]); + + const tableData = useMemo(() => { + if (!pivotQuery.data?.data) return []; + + return pivotQuery.data.data.map( + (row: { + sessionId: string; + distinctId: string; + createdAt: string; + propertyKeys: string[]; + propertyValues: string[]; + }) => { + const flat: Record = { + sessionId: row.sessionId, + distinctId: row.distinctId, + createdAt: row.createdAt, + }; + + row.propertyKeys.forEach((key, i) => { + flat[key] = row.propertyValues[i] ?? ''; + }); + + return flat; + }, + ); + }, [pivotQuery.data]); + + const tableQuery = useMemo( + () => (pivotQuery.data ? { ...pivotQuery.data, data: tableData } : undefined), + [pivotQuery.data, tableData], + ); + + const showPager = tableQuery && tableQuery.count > tableQuery.pageSize; + + const handlePageChange = (page: number) => { + router.push(updateParams({ page })); + }; + + const renderTruncatedText = (value: string) => ( + + + {value} + + + ); + + const renderTable = (displayMode: 'cards' | 'table') => ( + + + {(row: any) => ( + + + + )} + + {propertyKeys.map(key => ( + + {(row: any) => renderTruncatedText(row[key] ?? '')} + + ))} + + {(row: any) => renderTruncatedText(row.distinctId || '')} + + + {(row: any) => } + + + ); + + return ( + + } + > + + {isMobile ? ( + renderTable('cards') + ) : ( +
+
+ {renderTable('table')} +
+
+ )} + {showPager && tableQuery && ( + + + + )} +
+
+
+ ); +} diff --git a/src/app/(main)/websites/[websiteId]/session-data/SessionStringPropertyView.tsx b/src/app/(main)/websites/[websiteId]/session-data/SessionStringPropertyView.tsx new file mode 100644 index 000000000..e8a623220 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/session-data/SessionStringPropertyView.tsx @@ -0,0 +1,53 @@ +'use client'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { useSessionDataStatsQuery } from '@/components/hooks'; +import type { PropertyFilter } from '@/lib/types'; +import { PropertyChart } from '@/components/property-data/PropertyChart'; +import { HighCardinalityPropertyChart } from './HighCardinalityPropertyChart'; + +const HIGH_CARDINALITY_NAME_PATTERN = + /(email|e-mail|name|(^|_)(id|uuid|token)($|_)|distinct_id|user_id|customer_id|client_id|member_id)/i; + +function isHighCardinalityProperty(propertyName: string, totalValues: number, uniqueValues: number) { + if (!totalValues) return false; + + const uniquenessRate = uniqueValues / totalValues; + const hasHighCardinalityName = HIGH_CARDINALITY_NAME_PATTERN.test(propertyName); + + return totalValues >= 20 && (hasHighCardinalityName || uniqueValues >= 20 || uniquenessRate >= 0.6); +} + +export function SessionStringPropertyView({ + websiteId, + propertyName, + propertyFilters = [], +}: { + websiteId: string; + propertyName: string; + propertyFilters?: PropertyFilter[]; +}) { + const statsQuery = useSessionDataStatsQuery(websiteId, propertyName, propertyFilters); + const rows = statsQuery.data ?? []; + const totalValues = rows.reduce((sum, row) => sum + row.sessions, 0); + const uniqueValues = rows.length; + const isHighCardinality = isHighCardinalityProperty( + propertyName, + totalValues, + uniqueValues, + ); + + return ( + + {isHighCardinality ? ( + + ) : ( + + )} + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx index f6377edb3..13f7b1c60 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx @@ -1,21 +1,58 @@ -import { Column, Grid, ListItem, Select } from '@umami/react-zen'; +import { Column, ComboBox, Grid, Label, ListItem, Row } from '@umami/react-zen'; import { useMemo, useState } from 'react'; -import { PieChart } from '@/components/charts/PieChart'; import { LoadingPanel } from '@/components/common/LoadingPanel'; -import { - useMessages, - useSessionDataPropertiesQuery, - useSessionDataValuesQuery, -} from '@/components/hooks'; -import { ListTable } from '@/components/metrics/ListTable'; -import { CHART_COLORS } from '@/lib/constants'; +import { useMessages, usePropertyFieldsQuery, useSessionDataPropertiesQuery } from '@/components/hooks'; +import { DATA_TYPE } from '@/lib/constants'; +import type { PropertyFilter } from '@/lib/types'; +import { PropertyChart } from '@/components/property-data/PropertyChart'; +import { PropertyDateChart } from '@/components/property-data/PropertyDateChart'; +import { PropertyFilterBar } from '@/components/property-data/PropertyFilterBar'; +import { PropertyFilterButton } from '@/components/property-data/PropertyFilterButton'; +import { PropertyNumericChart } from '@/components/property-data/PropertyNumericChart'; +import { Panel } from '@/components/common/Panel'; +import { SessionDataPivotTable } from '../session-data/SessionDataPivotTable'; +import { SessionStringPropertyView } from '../session-data/SessionStringPropertyView'; export function SessionProperties({ websiteId }: { websiteId: string }) { const [propertyName, setPropertyName] = useState(''); + const [propertyFilters, setPropertyFilters] = useState([]); const { t, labels } = useMessages(); - const { data, isLoading, isFetching, error } = useSessionDataPropertiesQuery(websiteId); + const { data, isLoading, isFetching, error } = usePropertyFieldsQuery('session', websiteId); + const { data: scopedData } = useSessionDataPropertiesQuery( + websiteId, + propertyName ? { selectedPropertyName: propertyName, propertyFilters } : undefined, + { enabled: !!propertyName }, + ); - const properties: string[] = data?.map(e => e.propertyName); + const properties = useMemo(() => { + if (!data) return []; + const seen = new Set(); + + return data.filter((field: { propertyName: string; dataType: number }) => { + if (seen.has(field.propertyName)) return false; + seen.add(field.propertyName); + return true; + }); + }, [data]); + + const selectedProperty = useMemo( + () => properties.find((field: { propertyName: string }) => field.propertyName === propertyName), + [properties, propertyName], + ); + const scopedProperties = useMemo(() => { + if (!scopedData) return []; + const seen = new Set(); + + return scopedData.filter((field: { propertyName: string }) => { + if (seen.has(field.propertyName)) return false; + seen.add(field.propertyName); + return true; + }); + }, [scopedData]); + const scopedPropertyKeys = useMemo( + () => scopedProperties.map((field: { propertyName: string }) => field.propertyName), + [scopedProperties], + ); return ( - + {data && ( - - + + + + {properties.map((field: { propertyName: string }) => ( + + {field.propertyName} + + ))} + + + + {propertyName && ( + + + + )}
)} - {propertyName && } + {propertyName && } + {propertyName && selectedProperty?.dataType === DATA_TYPE.number && ( + + )} + {propertyName && selectedProperty?.dataType === DATA_TYPE.date && ( + + )} + {propertyName && selectedProperty?.dataType === DATA_TYPE.string && ( + + )} + {propertyName && selectedProperty?.dataType === DATA_TYPE.boolean && ( + + )} + {propertyName && selectedProperty?.dataType === DATA_TYPE.array && ( + + )} + {propertyName && ( + + + + )}
); } - -const SessionValues = ({ websiteId, propertyName }) => { - const { data, isLoading, isFetching, error } = useSessionDataValuesQuery(websiteId, propertyName); - - const propertySum = useMemo(() => { - return data?.reduce((sum, { total }) => sum + total, 0) ?? 0; - }, [data]); - - const chartData = useMemo(() => { - if (!propertyName || !data) return null; - return { - labels: data.map(({ value }) => value), - datasets: [ - { - data: data.map(({ total }) => total), - backgroundColor: CHART_COLORS, - borderWidth: 0, - }, - ], - }; - }, [propertyName, data]); - - const tableData = useMemo(() => { - if (!propertyName || !data || propertySum === 0) return []; - return data.map(({ value, total }) => ({ - label: value, - count: total, - percent: 100 * (total / propertySum), - })); - }, [propertyName, data, propertySum]); - - return ( - - {data && ( - - - - - )} - - ); -}; diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx index f891f2ad7..f7eb9a868 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx @@ -24,16 +24,20 @@ export function SessionsPage({ websiteId }) { - - + + {t(labels.activity)} {t(labels.properties)} - + - + diff --git a/src/app/api/websites/[websiteId]/session-data-pivot/route.ts b/src/app/api/websites/[websiteId]/session-data-pivot/route.ts new file mode 100644 index 000000000..f27b17781 --- /dev/null +++ b/src/app/api/websites/[websiteId]/session-data-pivot/route.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; +import { parsePropertyFilters } from '@/lib/params'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams, pagingParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getSessionDataPivot } from '@/queries/sql/sessions/getSessionDataPivot'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + propertyName: z.string(), + ...filterParams, + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { propertyName, ...rest } = query; + const filters = await getQueryFilters(rest, websiteId); + const propertyFilters = parsePropertyFilters(query); + const result = await getSessionDataPivot(websiteId, propertyName, filters, propertyFilters); + + return json(result); +} diff --git a/src/app/api/websites/[websiteId]/session-data/array-series/route.ts b/src/app/api/websites/[websiteId]/session-data/array-series/route.ts new file mode 100644 index 000000000..5dd5b4b4a --- /dev/null +++ b/src/app/api/websites/[websiteId]/session-data/array-series/route.ts @@ -0,0 +1,40 @@ +import { z } from 'zod'; +import { parsePropertyFilters } from '@/lib/params'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams, timezoneParam, unitParam } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getSessionDataArraySeries } from '@/queries/sql'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + propertyName: z.string(), + timezone: timezoneParam.optional(), + unit: unitParam.optional(), + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { propertyName, ...rest } = query; + const filters = await getQueryFilters(rest, websiteId); + const propertyFilters = parsePropertyFilters(query); + const data = await getSessionDataArraySeries(websiteId, propertyName, filters, propertyFilters); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/session-data/date-series/route.ts b/src/app/api/websites/[websiteId]/session-data/date-series/route.ts new file mode 100644 index 000000000..a607ff9ac --- /dev/null +++ b/src/app/api/websites/[websiteId]/session-data/date-series/route.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; +import { parsePropertyFilters } from '@/lib/params'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams, timezoneParam } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getSessionDataDateSeries } from '@/queries/sql'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + propertyName: z.string(), + timezone: timezoneParam.optional(), + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { propertyName, ...rest } = query; + const filters = await getQueryFilters(rest, websiteId); + const propertyFilters = parsePropertyFilters(query); + const data = await getSessionDataDateSeries(websiteId, propertyName, filters, propertyFilters); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/session-data/numeric-series/route.ts b/src/app/api/websites/[websiteId]/session-data/numeric-series/route.ts new file mode 100644 index 000000000..09cf623b6 --- /dev/null +++ b/src/app/api/websites/[websiteId]/session-data/numeric-series/route.ts @@ -0,0 +1,41 @@ +import { z } from 'zod'; +import { parsePropertyFilters } from '@/lib/params'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams, timezoneParam, unitParam } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getSessionDataNumericSeries } from '@/queries/sql'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + propertyName: z.string(), + metric: z.enum(['sum', 'avg', 'count']).default('sum'), + timezone: timezoneParam.optional(), + unit: unitParam.optional(), + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { propertyName, metric, ...rest } = query; + const filters = await getQueryFilters(rest, websiteId); + const propertyFilters = parsePropertyFilters(query); + const data = await getSessionDataNumericSeries(websiteId, propertyName, metric, filters, propertyFilters); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/session-data/numeric-stats/route.ts b/src/app/api/websites/[websiteId]/session-data/numeric-stats/route.ts new file mode 100644 index 000000000..a27f1bc73 --- /dev/null +++ b/src/app/api/websites/[websiteId]/session-data/numeric-stats/route.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; +import { parsePropertyFilters } from '@/lib/params'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams, timezoneParam } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getSessionDataNumericStats } from '@/queries/sql'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + propertyName: z.string(), + timezone: timezoneParam.optional(), + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { propertyName, ...rest } = query; + const filters = await getQueryFilters(rest, websiteId); + const propertyFilters = parsePropertyFilters(query); + const data = await getSessionDataNumericStats(websiteId, propertyName, filters, propertyFilters); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/session-data/properties/route.ts b/src/app/api/websites/[websiteId]/session-data/properties/route.ts index 2d8db1535..61a5c13d9 100644 --- a/src/app/api/websites/[websiteId]/session-data/properties/route.ts +++ b/src/app/api/websites/[websiteId]/session-data/properties/route.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { parsePropertyFilters } from '@/lib/params'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; import { filterParams } from '@/lib/schema'; @@ -12,6 +13,7 @@ export async function GET( const schema = z.object({ startAt: z.coerce.number().int(), endAt: z.coerce.number().int(), + selectedPropertyName: z.string().optional(), ...filterParams, }); @@ -27,9 +29,16 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters(query, websiteId); + const { selectedPropertyName, ...rest } = query; + const filters = await getQueryFilters(rest, websiteId); + const propertyFilters = parsePropertyFilters(query); - const data = await getSessionDataProperties(websiteId, filters); + const data = await getSessionDataProperties( + websiteId, + filters, + propertyFilters, + selectedPropertyName, + ); return json(data); } diff --git a/src/app/api/websites/[websiteId]/session-data/property-series/route.ts b/src/app/api/websites/[websiteId]/session-data/property-series/route.ts new file mode 100644 index 000000000..fc054e6e2 --- /dev/null +++ b/src/app/api/websites/[websiteId]/session-data/property-series/route.ts @@ -0,0 +1,40 @@ +import { z } from 'zod'; +import { parsePropertyFilters } from '@/lib/params'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams, timezoneParam, unitParam } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getSessionDataPropertySeries } from '@/queries/sql'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + propertyName: z.string(), + timezone: timezoneParam.optional(), + unit: unitParam.optional(), + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { propertyName, ...rest } = query; + const filters = await getQueryFilters(rest, websiteId); + const propertyFilters = parsePropertyFilters(query); + const data = await getSessionDataPropertySeries(websiteId, propertyName, filters, propertyFilters); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/session-data/stats/route.ts b/src/app/api/websites/[websiteId]/session-data/stats/route.ts new file mode 100644 index 000000000..c3a97fed6 --- /dev/null +++ b/src/app/api/websites/[websiteId]/session-data/stats/route.ts @@ -0,0 +1,40 @@ +import { z } from 'zod'; +import { parsePropertyFilters } from '@/lib/params'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams, timezoneParam, unitParam } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getSessionDataStats } from '@/queries/sql/sessions/getSessionDataStats'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + propertyName: z.string(), + timezone: timezoneParam.optional(), + unit: unitParam.optional(), + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { propertyName, ...rest } = query; + const filters = await getQueryFilters(rest, websiteId); + const propertyFilters = parsePropertyFilters(query); + const result = await getSessionDataStats(websiteId, propertyName, filters, propertyFilters); + + return json(result); +} diff --git a/src/app/api/websites/[websiteId]/session-data/values/route.ts b/src/app/api/websites/[websiteId]/session-data/values/route.ts index 7d06870ac..57b27e300 100644 --- a/src/app/api/websites/[websiteId]/session-data/values/route.ts +++ b/src/app/api/websites/[websiteId]/session-data/values/route.ts @@ -13,6 +13,7 @@ export async function GET( startAt: z.coerce.number().int(), endAt: z.coerce.number().int(), propertyName: z.string().optional(), + dataType: z.coerce.number().int().optional(), ...filterParams, }); @@ -28,12 +29,13 @@ export async function GET( return unauthorized(); } - const { propertyName } = query; + const { propertyName, dataType } = query; const filters = await getQueryFilters(query, websiteId); const data = await getSessionDataValues(websiteId, { ...filters, propertyName, + dataType, }); return json(data); diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts index d5719d404..bd508317d 100644 --- a/src/components/hooks/index.ts +++ b/src/components/hooks/index.ts @@ -17,17 +17,9 @@ export * from './queries/useBoardsQuery'; export * from './queries/useDashboardQuery'; export * from './queries/useDateRangeQuery'; export * from './queries/useDeleteQuery'; -export * from './queries/useEventDataArraySeriesQuery'; -export * from './queries/useEventDataDateSeriesQuery'; -export * from './queries/useEventDataEventsQuery'; -export * from './queries/useEventDataFieldsQuery'; -export * from './queries/useEventDataNumericSeriesQuery'; -export * from './queries/useEventDataNumericStatsQuery'; export * from './queries/useEventDataPivotQuery'; export * from './queries/useEventDataPropertiesQuery'; -export * from './queries/useEventDataPropertySeriesQuery'; export * from './queries/useEventDataQuery'; -export * from './queries/useEventDataValuesQuery'; export * from './queries/useLinkQuery'; export * from './queries/useLinkSharesQuery'; export * from './queries/useLinksQuery'; @@ -35,6 +27,13 @@ export * from './queries/useLoginQuery'; export * from './queries/usePixelQuery'; export * from './queries/usePixelSharesQuery'; export * from './queries/usePixelsQuery'; +export * from './queries/usePropertyArraySeriesQuery'; +export * from './queries/usePropertyDateSeriesQuery'; +export * from './queries/usePropertyFieldsQuery'; +export * from './queries/usePropertyNumericSeriesQuery'; +export * from './queries/usePropertyNumericStatsQuery'; +export * from './queries/usePropertySeriesQuery'; +export * from './queries/usePropertyValuesQuery'; export * from './queries/useRealtimeQuery'; export * from './queries/useReplayQuery'; export * from './queries/useReplaySavedQuery'; @@ -46,7 +45,9 @@ export * from './queries/useRevenueSessionsQuery'; export * from './queries/useSavedReplaysQuery'; export * from './queries/useSessionActivityQuery'; export * from './queries/useSessionDataPropertiesQuery'; +export * from './queries/useSessionDataPivotQuery'; export * from './queries/useSessionDataQuery'; +export * from './queries/useSessionDataStatsQuery'; export * from './queries/useSessionDataValuesQuery'; export * from './queries/useSessionReplaysQuery'; export * from './queries/useShareTokenQuery'; diff --git a/src/components/hooks/queries/useEventDataArraySeriesQuery.ts b/src/components/hooks/queries/useEventDataArraySeriesQuery.ts deleted file mode 100644 index dba01c8f1..000000000 --- a/src/components/hooks/queries/useEventDataArraySeriesQuery.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { serializeEventPropertyFilters } from '@/lib/params'; -import type { EventDataSeriesPoint, EventPropertyFilter, ReactQueryOptions } from '@/lib/types'; -import { useApi } from '../useApi'; -import { useDateParameters } from '../useDateParameters'; -import { useFilterParameters } from '../useFilterParameters'; - -export function useEventDataArraySeriesQuery( - websiteId: string, - eventName: string, - propertyName: string, - eventFilters: EventPropertyFilter[] = [], - options?: ReactQueryOptions, -) { - const { get, useQuery } = useApi(); - const { startAt, endAt, unit, timezone } = useDateParameters(); - const params = useFilterParameters(); - - return useQuery({ - queryKey: [ - 'websites:event-data-pivot:array-series', - { - websiteId, - eventName, - propertyName, - eventFilters, - startAt, - endAt, - unit, - timezone, - ...params, - }, - ], - queryFn: () => - get(`/websites/${websiteId}/event-data-pivot/array-series`, { - eventName, - propertyName, - startAt, - endAt, - unit, - timezone, - ...serializeEventPropertyFilters(eventFilters), - ...params, - }), - enabled: !!(websiteId && eventName && propertyName), - ...options, - }); -} diff --git a/src/components/hooks/queries/useEventDataDateSeriesQuery.ts b/src/components/hooks/queries/useEventDataDateSeriesQuery.ts deleted file mode 100644 index 606395b54..000000000 --- a/src/components/hooks/queries/useEventDataDateSeriesQuery.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { serializeEventPropertyFilters } from '@/lib/params'; -import type { EventDataDateSeriesPoint, EventPropertyFilter, ReactQueryOptions } from '@/lib/types'; -import { useApi } from '../useApi'; -import { useDateParameters } from '../useDateParameters'; -import { useFilterParameters } from '../useFilterParameters'; - -export function useEventDataDateSeriesQuery( - websiteId: string, - eventName: string, - propertyName: string, - eventFilters: EventPropertyFilter[] = [], - options?: ReactQueryOptions, -) { - const { get, useQuery } = useApi(); - const { startAt, endAt, timezone } = useDateParameters(); - const params = useFilterParameters(); - - return useQuery({ - queryKey: [ - 'websites:event-data-pivot:date-series', - { - websiteId, - eventName, - propertyName, - eventFilters, - startAt, - endAt, - timezone, - ...params, - }, - ], - queryFn: () => - get(`/websites/${websiteId}/event-data-pivot/date-series`, { - eventName, - propertyName, - startAt, - endAt, - timezone, - ...serializeEventPropertyFilters(eventFilters), - ...params, - }), - enabled: !!(websiteId && eventName && propertyName), - ...options, - }); -} diff --git a/src/components/hooks/queries/useEventDataEventsQuery.ts b/src/components/hooks/queries/useEventDataEventsQuery.ts deleted file mode 100644 index 1401989f3..000000000 --- a/src/components/hooks/queries/useEventDataEventsQuery.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { ReactQueryOptions } from '@/lib/types'; -import { useApi } from '../useApi'; -import { useDateParameters } from '../useDateParameters'; -import { useFilterParameters } from '../useFilterParameters'; - -export function useEventDataEventsQuery(websiteId: string, options?: ReactQueryOptions) { - const { get, useQuery } = useApi(); - const { startAt, endAt, unit, timezone } = useDateParameters(); - const filters = useFilterParameters(); - - return useQuery({ - queryKey: [ - 'websites:event-data:events', - { websiteId, startAt, endAt, unit, timezone, ...filters }, - ], - queryFn: () => - get(`/websites/${websiteId}/event-data/events`, { - startAt, - endAt, - unit, - timezone, - ...filters, - }), - enabled: !!websiteId, - ...options, - }); -} diff --git a/src/components/hooks/queries/useEventDataFieldsQuery.ts b/src/components/hooks/queries/useEventDataFieldsQuery.ts deleted file mode 100644 index bd9e0f5ce..000000000 --- a/src/components/hooks/queries/useEventDataFieldsQuery.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { ReactQueryOptions } from '@/lib/types'; -import { useApi } from '../useApi'; -import { useDateParameters } from '../useDateParameters'; -import { useFilterParameters } from '../useFilterParameters'; - -export function useEventDataFieldsQuery( - websiteId: string, - eventName?: string, - options?: ReactQueryOptions, -) { - const { get, useQuery } = useApi(); - const { startAt, endAt, unit, timezone } = useDateParameters(); - const params = useFilterParameters(); - - return useQuery({ - queryKey: [ - 'websites:event-data:fields', - { websiteId, eventName, startAt, endAt, unit, timezone, ...params }, - ], - queryFn: () => - get(`/websites/${websiteId}/event-data/fields`, { - event: eventName, - startAt, - endAt, - unit, - timezone, - ...params, - }), - enabled: !!websiteId, - ...options, - }); -} diff --git a/src/components/hooks/queries/useEventDataNumericSeriesQuery.ts b/src/components/hooks/queries/useEventDataNumericSeriesQuery.ts deleted file mode 100644 index 26804ed3b..000000000 --- a/src/components/hooks/queries/useEventDataNumericSeriesQuery.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { serializeEventPropertyFilters } from '@/lib/params'; -import type { EventPropertyFilter, ReactQueryOptions } from '@/lib/types'; -import { useApi } from '../useApi'; -import { useDateParameters } from '../useDateParameters'; -import { useFilterParameters } from '../useFilterParameters'; - -export function useEventDataNumericSeriesQuery( - websiteId: string, - eventName: string, - propertyName: string, - metric: 'sum' | 'avg' | 'count', - eventFilters: EventPropertyFilter[] = [], - options?: ReactQueryOptions, -) { - const { get, useQuery } = useApi(); - const { startAt, endAt, unit, timezone } = useDateParameters(); - const params = useFilterParameters(); - - return useQuery({ - queryKey: [ - 'websites:event-data-pivot:numeric-series', - { websiteId, eventName, propertyName, metric, eventFilters, startAt, endAt, unit, timezone, ...params }, - ], - queryFn: () => - get(`/websites/${websiteId}/event-data-pivot/numeric-series`, { - eventName, - propertyName, - metric, - startAt, - endAt, - unit, - timezone, - ...serializeEventPropertyFilters(eventFilters), - ...params, - }), - enabled: !!(websiteId && eventName && propertyName), - ...options, - }); -} diff --git a/src/components/hooks/queries/useEventDataNumericStatsQuery.ts b/src/components/hooks/queries/useEventDataNumericStatsQuery.ts deleted file mode 100644 index f52e44ca5..000000000 --- a/src/components/hooks/queries/useEventDataNumericStatsQuery.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { serializeEventPropertyFilters } from '@/lib/params'; -import type { EventDataNumericStats, EventPropertyFilter, ReactQueryOptions } from '@/lib/types'; -import { useApi } from '../useApi'; -import { useDateParameters } from '../useDateParameters'; -import { useFilterParameters } from '../useFilterParameters'; - -export function useEventDataNumericStatsQuery( - websiteId: string, - eventName: string, - propertyName: string, - eventFilters: EventPropertyFilter[] = [], - options?: ReactQueryOptions, -) { - const { get, useQuery } = useApi(); - const { startAt, endAt, timezone } = useDateParameters(); - const params = useFilterParameters(); - - return useQuery({ - queryKey: [ - 'websites:event-data-pivot:numeric-stats', - { websiteId, eventName, propertyName, eventFilters, startAt, endAt, timezone, ...params }, - ], - queryFn: () => - get(`/websites/${websiteId}/event-data-pivot/numeric-stats`, { - eventName, - propertyName, - startAt, - endAt, - timezone, - ...serializeEventPropertyFilters(eventFilters), - ...params, - }), - enabled: !!(websiteId && eventName && propertyName), - ...options, - }); -} diff --git a/src/components/hooks/queries/useEventDataPropertiesQuery.ts b/src/components/hooks/queries/useEventDataPropertiesQuery.ts index dfa6e9295..ef921824e 100644 --- a/src/components/hooks/queries/useEventDataPropertiesQuery.ts +++ b/src/components/hooks/queries/useEventDataPropertiesQuery.ts @@ -6,7 +6,7 @@ import { useFilterParameters } from '../useFilterParameters'; export function useEventDataPropertiesQuery(websiteId: string, options?: ReactQueryOptions) { const { get, useQuery } = useApi(); const { startAt, endAt, unit, timezone } = useDateParameters(); - const filters = useFilterParameters(); + const filters = useFilterParameters({ includePagination: false }); return useQuery({ queryKey: [ diff --git a/src/components/hooks/queries/useEventDataPropertySeriesQuery.ts b/src/components/hooks/queries/useEventDataPropertySeriesQuery.ts deleted file mode 100644 index 2d94d8cf4..000000000 --- a/src/components/hooks/queries/useEventDataPropertySeriesQuery.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { serializeEventPropertyFilters } from '@/lib/params'; -import type { EventDataSeriesPoint, EventPropertyFilter, ReactQueryOptions } from '@/lib/types'; -import { useApi } from '../useApi'; -import { useDateParameters } from '../useDateParameters'; -import { useFilterParameters } from '../useFilterParameters'; - -export function useEventDataPropertySeriesQuery( - websiteId: string, - eventName: string, - propertyName: string, - eventFilters: EventPropertyFilter[] = [], - options?: ReactQueryOptions, -) { - const { get, useQuery } = useApi(); - const { startAt, endAt, unit, timezone } = useDateParameters(); - const params = useFilterParameters(); - - return useQuery({ - queryKey: [ - 'websites:event-data-pivot:property-series', - { websiteId, eventName, propertyName, eventFilters, startAt, endAt, unit, timezone, ...params }, - ], - queryFn: () => - get(`/websites/${websiteId}/event-data-pivot/property-series`, { - eventName, - propertyName, - startAt, - endAt, - unit, - timezone, - ...serializeEventPropertyFilters(eventFilters), - ...params, - }), - enabled: !!(websiteId && eventName && propertyName), - ...options, - }); -} diff --git a/src/components/hooks/queries/useEventStatsQuery.ts b/src/components/hooks/queries/useEventStatsQuery.ts index b3ca60eaa..6c61e75f5 100644 --- a/src/components/hooks/queries/useEventStatsQuery.ts +++ b/src/components/hooks/queries/useEventStatsQuery.ts @@ -26,7 +26,7 @@ export function useEventStatsQuery( ) { const { get, useQuery } = useApi(); const { startAt, endAt } = useDateParameters(); - const filters = useFilterParameters(); + const filters = useFilterParameters({ includePagination: false }); return useQuery({ queryKey: ['websites:events:stats', { websiteId, startAt, endAt, ...filters }], diff --git a/src/components/hooks/queries/usePropertyArraySeriesQuery.ts b/src/components/hooks/queries/usePropertyArraySeriesQuery.ts new file mode 100644 index 000000000..fd4b8a42c --- /dev/null +++ b/src/components/hooks/queries/usePropertyArraySeriesQuery.ts @@ -0,0 +1,44 @@ +import { serializePropertyFilters } from '@/lib/params'; +import type { EventDataSeriesPoint, PropertyFilter, ReactQueryOptions } from '@/lib/types'; +import { useApi } from '../useApi'; +import { useDateParameters } from '../useDateParameters'; +import { useFilterParameters } from '../useFilterParameters'; +import type { PropertyDataSource } from './usePropertyFieldsQuery'; + +export function usePropertyArraySeriesQuery( + source: PropertyDataSource, + websiteId: string, + propertyName: string, + propertyFilters: PropertyFilter[] = [], + eventName?: string, + options?: ReactQueryOptions, +) { + const { get, useQuery } = useApi(); + const { startAt, endAt, unit, timezone } = useDateParameters(); + const params = useFilterParameters({ includePagination: false }); + + return useQuery({ + queryKey: [ + `websites:${source}-data:array-series`, + { websiteId, propertyName, eventName, propertyFilters, startAt, endAt, unit, timezone, ...params }, + ], + queryFn: () => + get( + source === 'event' + ? `/websites/${websiteId}/event-data-pivot/array-series` + : `/websites/${websiteId}/session-data/array-series`, + { + ...(source === 'event' ? { eventName } : {}), + propertyName, + startAt, + endAt, + unit, + timezone, + ...serializePropertyFilters(propertyFilters), + ...params, + }, + ), + enabled: !!(websiteId && propertyName && (source === 'session' || eventName)), + ...options, + }); +} diff --git a/src/components/hooks/queries/usePropertyDateSeriesQuery.ts b/src/components/hooks/queries/usePropertyDateSeriesQuery.ts new file mode 100644 index 000000000..4c34c0400 --- /dev/null +++ b/src/components/hooks/queries/usePropertyDateSeriesQuery.ts @@ -0,0 +1,43 @@ +import { serializePropertyFilters } from '@/lib/params'; +import type { EventDataDateSeriesPoint, PropertyFilter, ReactQueryOptions } from '@/lib/types'; +import { useApi } from '../useApi'; +import { useDateParameters } from '../useDateParameters'; +import { useFilterParameters } from '../useFilterParameters'; +import type { PropertyDataSource } from './usePropertyFieldsQuery'; + +export function usePropertyDateSeriesQuery( + source: PropertyDataSource, + websiteId: string, + propertyName: string, + propertyFilters: PropertyFilter[] = [], + eventName?: string, + options?: ReactQueryOptions, +) { + const { get, useQuery } = useApi(); + const { startAt, endAt, timezone } = useDateParameters(); + const params = useFilterParameters({ includePagination: false }); + + return useQuery({ + queryKey: [ + `websites:${source}-data:date-series`, + { websiteId, propertyName, eventName, propertyFilters, startAt, endAt, timezone, ...params }, + ], + queryFn: () => + get( + source === 'event' + ? `/websites/${websiteId}/event-data-pivot/date-series` + : `/websites/${websiteId}/session-data/date-series`, + { + ...(source === 'event' ? { eventName } : {}), + propertyName, + startAt, + endAt, + timezone, + ...serializePropertyFilters(propertyFilters), + ...params, + }, + ), + enabled: !!(websiteId && propertyName && (source === 'session' || eventName)), + ...options, + }); +} diff --git a/src/components/hooks/queries/usePropertyFieldsQuery.ts b/src/components/hooks/queries/usePropertyFieldsQuery.ts new file mode 100644 index 000000000..48cfea9e7 --- /dev/null +++ b/src/components/hooks/queries/usePropertyFieldsQuery.ts @@ -0,0 +1,40 @@ +import type { ReactQueryOptions } from '@/lib/types'; +import { useApi } from '../useApi'; +import { useDateParameters } from '../useDateParameters'; +import { useFilterParameters } from '../useFilterParameters'; + +export type PropertyDataSource = 'event' | 'session'; + +export function usePropertyFieldsQuery( + source: PropertyDataSource, + websiteId: string, + eventName?: string, + options?: ReactQueryOptions, +) { + const { get, useQuery } = useApi(); + const { startAt, endAt, unit, timezone } = useDateParameters(); + const params = useFilterParameters({ includePagination: false }); + + return useQuery({ + queryKey: [ + `websites:${source}-data:fields`, + { websiteId, eventName, startAt, endAt, unit, timezone, ...params }, + ], + queryFn: () => + get( + source === 'event' + ? `/websites/${websiteId}/event-data/fields` + : `/websites/${websiteId}/session-data/properties`, + { + ...(source === 'event' ? { event: eventName } : {}), + startAt, + endAt, + unit, + timezone, + ...params, + }, + ), + enabled: !!(websiteId && (source === 'session' || eventName)), + ...options, + }); +} diff --git a/src/components/hooks/queries/usePropertyNumericSeriesQuery.ts b/src/components/hooks/queries/usePropertyNumericSeriesQuery.ts new file mode 100644 index 000000000..ffe5b18b7 --- /dev/null +++ b/src/components/hooks/queries/usePropertyNumericSeriesQuery.ts @@ -0,0 +1,46 @@ +import { serializePropertyFilters } from '@/lib/params'; +import type { PropertyFilter, ReactQueryOptions } from '@/lib/types'; +import { useApi } from '../useApi'; +import { useDateParameters } from '../useDateParameters'; +import { useFilterParameters } from '../useFilterParameters'; +import type { PropertyDataSource } from './usePropertyFieldsQuery'; + +export function usePropertyNumericSeriesQuery( + source: PropertyDataSource, + websiteId: string, + propertyName: string, + metric: 'sum' | 'avg' | 'count', + propertyFilters: PropertyFilter[] = [], + eventName?: string, + options?: ReactQueryOptions, +) { + const { get, useQuery } = useApi(); + const { startAt, endAt, unit, timezone } = useDateParameters(); + const params = useFilterParameters({ includePagination: false }); + + return useQuery({ + queryKey: [ + `websites:${source}-data:numeric-series`, + { websiteId, propertyName, metric, eventName, propertyFilters, startAt, endAt, unit, timezone, ...params }, + ], + queryFn: () => + get( + source === 'event' + ? `/websites/${websiteId}/event-data-pivot/numeric-series` + : `/websites/${websiteId}/session-data/numeric-series`, + { + ...(source === 'event' ? { eventName } : {}), + propertyName, + metric, + startAt, + endAt, + unit, + timezone, + ...serializePropertyFilters(propertyFilters), + ...params, + }, + ), + enabled: !!(websiteId && propertyName && (source === 'session' || eventName)), + ...options, + }); +} diff --git a/src/components/hooks/queries/usePropertyNumericStatsQuery.ts b/src/components/hooks/queries/usePropertyNumericStatsQuery.ts new file mode 100644 index 000000000..01cc48568 --- /dev/null +++ b/src/components/hooks/queries/usePropertyNumericStatsQuery.ts @@ -0,0 +1,43 @@ +import { serializePropertyFilters } from '@/lib/params'; +import type { EventDataNumericStats, PropertyFilter, ReactQueryOptions } from '@/lib/types'; +import { useApi } from '../useApi'; +import { useDateParameters } from '../useDateParameters'; +import { useFilterParameters } from '../useFilterParameters'; +import type { PropertyDataSource } from './usePropertyFieldsQuery'; + +export function usePropertyNumericStatsQuery( + source: PropertyDataSource, + websiteId: string, + propertyName: string, + propertyFilters: PropertyFilter[] = [], + eventName?: string, + options?: ReactQueryOptions, +) { + const { get, useQuery } = useApi(); + const { startAt, endAt, timezone } = useDateParameters(); + const params = useFilterParameters({ includePagination: false }); + + return useQuery({ + queryKey: [ + `websites:${source}-data:numeric-stats`, + { websiteId, propertyName, eventName, propertyFilters, startAt, endAt, timezone, ...params }, + ], + queryFn: () => + get( + source === 'event' + ? `/websites/${websiteId}/event-data-pivot/numeric-stats` + : `/websites/${websiteId}/session-data/numeric-stats`, + { + ...(source === 'event' ? { eventName } : {}), + propertyName, + startAt, + endAt, + timezone, + ...serializePropertyFilters(propertyFilters), + ...params, + }, + ), + enabled: !!(websiteId && propertyName && (source === 'session' || eventName)), + ...options, + }); +} diff --git a/src/components/hooks/queries/usePropertySeriesQuery.ts b/src/components/hooks/queries/usePropertySeriesQuery.ts new file mode 100644 index 000000000..3df8af796 --- /dev/null +++ b/src/components/hooks/queries/usePropertySeriesQuery.ts @@ -0,0 +1,44 @@ +import { serializePropertyFilters } from '@/lib/params'; +import type { EventDataSeriesPoint, PropertyFilter, ReactQueryOptions } from '@/lib/types'; +import { useApi } from '../useApi'; +import { useDateParameters } from '../useDateParameters'; +import { useFilterParameters } from '../useFilterParameters'; +import type { PropertyDataSource } from './usePropertyFieldsQuery'; + +export function usePropertySeriesQuery( + source: PropertyDataSource, + websiteId: string, + propertyName: string, + propertyFilters: PropertyFilter[] = [], + eventName?: string, + options?: ReactQueryOptions, +) { + const { get, useQuery } = useApi(); + const { startAt, endAt, unit, timezone } = useDateParameters(); + const params = useFilterParameters({ includePagination: false }); + + return useQuery({ + queryKey: [ + `websites:${source}-data:property-series`, + { websiteId, propertyName, eventName, propertyFilters, startAt, endAt, unit, timezone, ...params }, + ], + queryFn: () => + get( + source === 'event' + ? `/websites/${websiteId}/event-data-pivot/property-series` + : `/websites/${websiteId}/session-data/property-series`, + { + ...(source === 'event' ? { eventName } : {}), + propertyName, + startAt, + endAt, + unit, + timezone, + ...serializePropertyFilters(propertyFilters), + ...params, + }, + ), + enabled: !!(websiteId && propertyName && (source === 'session' || eventName)), + ...options, + }); +} diff --git a/src/components/hooks/queries/usePropertyValuesQuery.ts b/src/components/hooks/queries/usePropertyValuesQuery.ts new file mode 100644 index 000000000..131620fae --- /dev/null +++ b/src/components/hooks/queries/usePropertyValuesQuery.ts @@ -0,0 +1,46 @@ +import { serializePropertyFilters } from '@/lib/params'; +import type { PropertyFilter, ReactQueryOptions } from '@/lib/types'; +import { useApi } from '../useApi'; +import { useDateParameters } from '../useDateParameters'; +import { useFilterParameters } from '../useFilterParameters'; +import type { PropertyDataSource } from './usePropertyFieldsQuery'; + +export function usePropertyValuesQuery( + source: PropertyDataSource, + websiteId: string, + propertyName: string, + dataType?: number, + propertyFilters: PropertyFilter[] = [], + eventName?: string, + options?: ReactQueryOptions, +) { + const { get, useQuery } = useApi(); + const { startAt, endAt, unit, timezone } = useDateParameters(); + const params = useFilterParameters({ includePagination: false }); + + return useQuery({ + queryKey: [ + `websites:${source}-data:values`, + { websiteId, propertyName, dataType, eventName, propertyFilters, startAt, endAt, unit, timezone, ...params }, + ], + queryFn: () => + get( + source === 'event' + ? `/websites/${websiteId}/event-data/values` + : `/websites/${websiteId}/session-data/values`, + { + ...(source === 'event' ? { event: eventName } : {}), + startAt, + endAt, + unit, + timezone, + propertyName, + dataType, + ...serializePropertyFilters(propertyFilters), + ...params, + }, + ), + enabled: !!(websiteId && propertyName && (source === 'session' || eventName)), + ...options, + }); +} diff --git a/src/components/hooks/queries/useResultQuery.ts b/src/components/hooks/queries/useResultQuery.ts index 1828e594a..785b05694 100644 --- a/src/components/hooks/queries/useResultQuery.ts +++ b/src/components/hooks/queries/useResultQuery.ts @@ -11,7 +11,7 @@ export function useResultQuery( const { websiteId, ...parameters } = params; const { post, useQuery } = useApi(); const { startDate, endDate, timezone, unit } = useDateParameters(); - const filters = useFilterParameters(); + const filters = useFilterParameters({ includePagination: false }); return useQuery({ queryKey: [ diff --git a/src/components/hooks/queries/useEventDataValuesQuery.ts b/src/components/hooks/queries/useSessionDataPivotQuery.ts similarity index 50% rename from src/components/hooks/queries/useEventDataValuesQuery.ts rename to src/components/hooks/queries/useSessionDataPivotQuery.ts index 3141e41d1..db5fecccf 100644 --- a/src/components/hooks/queries/useEventDataValuesQuery.ts +++ b/src/components/hooks/queries/useSessionDataPivotQuery.ts @@ -1,34 +1,33 @@ -import type { ReactQueryOptions } from '@/lib/types'; +import { serializePropertyFilters } from '@/lib/params'; +import type { PropertyFilter, ReactQueryOptions } from '@/lib/types'; import { useApi } from '../useApi'; import { useDateParameters } from '../useDateParameters'; import { useFilterParameters } from '../useFilterParameters'; -export function useEventDataValuesQuery( +export function useSessionDataPivotQuery( websiteId: string, - event: string, propertyName: string, - dataType?: number, + propertyFilters: PropertyFilter[] = [], options?: ReactQueryOptions, ) { const { get, useQuery } = useApi(); const { startAt, endAt, unit, timezone } = useDateParameters(); - const filters = useFilterParameters(); + const params = useFilterParameters(); - return useQuery({ + return useQuery({ queryKey: [ - 'websites:event-data:values', - { websiteId, startAt, endAt, unit, timezone, ...filters, event, propertyName, dataType }, + 'websites:session-data-pivot', + { websiteId, propertyName, propertyFilters, startAt, endAt, unit, timezone, ...params }, ], queryFn: () => - get(`/websites/${websiteId}/event-data/values`, { + get(`/websites/${websiteId}/session-data-pivot`, { startAt, endAt, unit, timezone, - ...filters, - event, propertyName, - dataType, + ...serializePropertyFilters(propertyFilters), + ...params, }), enabled: !!(websiteId && propertyName), ...options, diff --git a/src/components/hooks/queries/useSessionDataPropertiesQuery.ts b/src/components/hooks/queries/useSessionDataPropertiesQuery.ts index ac651bb96..fa52ae302 100644 --- a/src/components/hooks/queries/useSessionDataPropertiesQuery.ts +++ b/src/components/hooks/queries/useSessionDataPropertiesQuery.ts @@ -1,17 +1,36 @@ import type { ReactQueryOptions } from '@/lib/types'; +import { serializePropertyFilters } from '@/lib/params'; +import type { PropertyFilter } from '@/lib/types'; import { useApi } from '../useApi'; import { useDateParameters } from '../useDateParameters'; import { useFilterParameters } from '../useFilterParameters'; -export function useSessionDataPropertiesQuery(websiteId: string, options?: ReactQueryOptions) { +export function useSessionDataPropertiesQuery( + websiteId: string, + params?: { + selectedPropertyName?: string; + propertyFilters?: PropertyFilter[]; + }, + options?: ReactQueryOptions, +) { const { get, useQuery } = useApi(); const { startAt, endAt, unit, timezone } = useDateParameters(); - const filters = useFilterParameters(); + const filters = useFilterParameters({ includePagination: false }); + const { selectedPropertyName, propertyFilters = [] } = params || {}; return useQuery({ queryKey: [ 'websites:session-data:properties', - { websiteId, startAt, endAt, unit, timezone, ...filters }, + { + websiteId, + selectedPropertyName, + propertyFilters, + startAt, + endAt, + unit, + timezone, + ...filters, + }, ], queryFn: () => get(`/websites/${websiteId}/session-data/properties`, { @@ -19,6 +38,8 @@ export function useSessionDataPropertiesQuery(websiteId: string, options?: React endAt, unit, timezone, + selectedPropertyName, + ...serializePropertyFilters(propertyFilters), ...filters, }), enabled: !!websiteId, diff --git a/src/components/hooks/queries/useSessionDataStatsQuery.ts b/src/components/hooks/queries/useSessionDataStatsQuery.ts new file mode 100644 index 000000000..8380fedf5 --- /dev/null +++ b/src/components/hooks/queries/useSessionDataStatsQuery.ts @@ -0,0 +1,35 @@ +import { serializePropertyFilters } from '@/lib/params'; +import type { PropertyFilter, ReactQueryOptions } from '@/lib/types'; +import { useApi } from '../useApi'; +import { useDateParameters } from '../useDateParameters'; +import { useFilterParameters } from '../useFilterParameters'; + +export function useSessionDataStatsQuery( + websiteId: string, + propertyName: string, + propertyFilters: PropertyFilter[] = [], + options?: ReactQueryOptions, +) { + const { get, useQuery } = useApi(); + const { startAt, endAt, unit, timezone } = useDateParameters(); + const params = useFilterParameters({ includePagination: false }); + + return useQuery({ + queryKey: [ + 'websites:session-data:stats', + { websiteId, propertyName, propertyFilters, startAt, endAt, unit, timezone, ...params }, + ], + queryFn: () => + get(`/websites/${websiteId}/session-data/stats`, { + startAt, + endAt, + unit, + timezone, + propertyName, + ...serializePropertyFilters(propertyFilters), + ...params, + }), + enabled: !!(websiteId && propertyName), + ...options, + }); +} diff --git a/src/components/hooks/useFilterParameters.ts b/src/components/hooks/useFilterParameters.ts index 3579bef95..24211c386 100644 --- a/src/components/hooks/useFilterParameters.ts +++ b/src/components/hooks/useFilterParameters.ts @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { FILTER_COLUMNS } from '@/lib/constants'; import { useNavigation } from './useNavigation'; -export function useFilterParameters() { +export function useFilterParameters({ includePagination = true }: { includePagination?: boolean } = {}) { const { query } = useNavigation(); return useMemo(() => { @@ -15,15 +15,20 @@ export function useFilterParameters() { } } - return { + const params = { ...filterParams, search: query.search, segment: query.segment, cohort: query.cohort, excludeBounce: query.excludeBounce, match: query.match, - page: query.page, - pageSize: query.pageSize, - }; - }, [query]); + } as Record; + + if (includePagination) { + params.page = query.page; + params.pageSize = query.pageSize; + } + + return params; + }, [query, includePagination]); } diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataPropertyChart.tsx b/src/components/property-data/PropertyChart.tsx similarity index 84% rename from src/app/(main)/websites/[websiteId]/event-data/EventDataPropertyChart.tsx rename to src/components/property-data/PropertyChart.tsx index 0fccff7c0..0700ff1a7 100644 --- a/src/app/(main)/websites/[websiteId]/event-data/EventDataPropertyChart.tsx +++ b/src/components/property-data/PropertyChart.tsx @@ -6,54 +6,58 @@ import { BarChart } from '@/components/charts/BarChart'; import { PieChart } from '@/components/charts/PieChart'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { - useEventDataArraySeriesQuery, useDateRange, - useEventDataPropertySeriesQuery, useLocale, useMessages, + usePropertyArraySeriesQuery, + usePropertySeriesQuery, useTimezone, } from '@/components/hooks'; +import type { PropertyDataSource } from '@/components/hooks/queries/usePropertyFieldsQuery'; import { ListTable } from '@/components/metrics/ListTable'; import { renderDateLabels } from '@/lib/charts'; import { CHART_COLORS } from '@/lib/constants'; import { generateTimeSeries } from '@/lib/date'; -import type { EventDataSeriesPoint, EventPropertyFilter } from '@/lib/types'; +import type { EventDataSeriesPoint, PropertyFilter } from '@/lib/types'; -export function EventDataPropertyChart({ +export function PropertyChart({ + source, websiteId, - eventName, propertyName, - eventFilters = [], + propertyFilters = [], + eventName, seriesType = 'property', }: { + source: PropertyDataSource; websiteId: string; - eventName: string; propertyName: string; - eventFilters?: EventPropertyFilter[]; + propertyFilters?: PropertyFilter[]; + eventName?: string; seriesType?: 'property' | 'array'; }) { const { t, labels } = useMessages(); const { timezone } = useTimezone(); const { dateRange: { startDate, endDate, unit } } = useDateRange({ timezone }); const { locale, dateLocale } = useLocale(); - const propertySeriesQuery = useEventDataPropertySeriesQuery( + const propertySeriesQuery = usePropertySeriesQuery( + source, websiteId, - eventName, propertyName, - eventFilters, + propertyFilters, + eventName, { enabled: seriesType === 'property' }, ); - const arraySeriesQuery = useEventDataArraySeriesQuery( + const arraySeriesQuery = usePropertyArraySeriesQuery( + source, websiteId, - eventName, propertyName, - eventFilters, + propertyFilters, + eventName, { enabled: seriesType === 'array' }, ); const { data, isLoading, isFetching, error } = seriesType === 'array' ? arraySeriesQuery : propertySeriesQuery; - // Aggregate totals per value from the already-filtered time series const aggregated = useMemo(() => { if (!data) return []; const totals = data.reduce((obj: Record, { x, y }) => { @@ -66,16 +70,17 @@ export function EventDataPropertyChart({ }, [data]); const valueLabels = useMemo(() => aggregated.map(({ value }) => value), [aggregated]); - - const colorMap = useMemo(() => { - return valueLabels.reduce( - (obj, label, index) => { - obj[label] = CHART_COLORS[index % CHART_COLORS.length]; - return obj; - }, - {} as Record, - ); - }, [valueLabels]); + const colorMap = useMemo( + () => + valueLabels.reduce( + (obj, label, index) => { + obj[label] = CHART_COLORS[index % CHART_COLORS.length]; + return obj; + }, + {} as Record, + ), + [valueLabels], + ); const chartData = useMemo(() => { if (!data) return; @@ -86,7 +91,7 @@ export function EventDataPropertyChart({ return obj; }, {}); - if (!map || Object.keys(map).length === 0) { + if (!Object.keys(map).length) { return { datasets: [ { @@ -125,12 +130,10 @@ export function EventDataPropertyChart({ }, [data, startDate, endDate, unit, dateLocale, valueLabels, colorMap]); const renderXLabel = useCallback(renderDateLabels(unit, locale), [unit, locale]); - const propertySum = useMemo( () => aggregated.reduce((sum, { total }) => sum + total, 0), [aggregated], ); - const tableData = useMemo(() => { if (!aggregated.length || propertySum === 0) return []; return aggregated.map(({ value, total }) => ({ @@ -139,7 +142,6 @@ export function EventDataPropertyChart({ percent: 100 * (total / propertySum), })); }, [aggregated, propertySum]); - const pieChartData = useMemo(() => { if (!aggregated.length) return null; return { diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataDateChart.tsx b/src/components/property-data/PropertyDateChart.tsx similarity index 51% rename from src/app/(main)/websites/[websiteId]/event-data/EventDataDateChart.tsx rename to src/components/property-data/PropertyDateChart.tsx index c075a46d5..4eb83d6ea 100644 --- a/src/app/(main)/websites/[websiteId]/event-data/EventDataDateChart.tsx +++ b/src/components/property-data/PropertyDateChart.tsx @@ -1,6 +1,5 @@ 'use client'; import { Column, Grid, Text, useTheme } from '@umami/react-zen'; -import { colord } from 'colord'; import { differenceInCalendarDays, format, @@ -17,15 +16,15 @@ import { type ReactNode, useCallback, useMemo } from 'react'; import { BarChart } from '@/components/charts/BarChart'; import { PieChart } from '@/components/charts/PieChart'; import { LoadingPanel } from '@/components/common/LoadingPanel'; -import { useEventDataDateSeriesQuery, useLocale, useMessages, useTimezone } from '@/components/hooks'; +import { useLocale, useMessages, usePropertyDateSeriesQuery, useTimezone } from '@/components/hooks'; +import type { PropertyDataSource } from '@/components/hooks/queries/usePropertyFieldsQuery'; import { ListTable } from '@/components/metrics/ListTable'; import { MetricsBar } from '@/components/metrics/MetricsBar'; import { renderDateLabels } from '@/lib/charts'; import { getThemeColors } from '@/lib/colors'; import { CHART_COLORS } from '@/lib/constants'; import { formatDate, generateTimeSeries } from '@/lib/date'; -import { formatLongNumber } from '@/lib/format'; -import type { EventDataDateSeriesPoint, EventPropertyFilter } from '@/lib/types'; +import type { EventDataDateSeriesPoint, PropertyFilter } from '@/lib/types'; type DateChartUnit = 'day' | 'week' | 'month' | 'year'; type ParsedDateRow = EventDataDateSeriesPoint & { date: Date }; @@ -40,15 +39,7 @@ function parseDateValue(value: string) { return isValid(date) ? date : null; } -function InsightCard({ - label, - value, - hint, -}: { - label: string; - value: string; - hint?: ReactNode; -}) { +function InsightCard({ label, value, hint }: { label: string; value: string; hint?: ReactNode }) { return ( - - {label} - - - {value} - - {hint && ( - - {hint} - - )} + {label} + {value} + {hint && {hint}} ); } function getChartUnit(minDate: Date, maxDate: Date): DateChartUnit { const spanDays = differenceInCalendarDays(maxDate, minDate); - if (spanDays <= 45) return 'day'; if (spanDays <= 180) return 'week'; if (spanDays <= 730) return 'month'; - return 'year'; } @@ -99,74 +80,67 @@ function getBucketStart(date: Date, unit: DateChartUnit, dateLocale: Locale) { } } -export function EventDataDateChart({ +export function PropertyDateChart({ + source, websiteId, - eventName, propertyName, - eventFilters = [], + propertyFilters = [], + eventName, }: { + source: PropertyDataSource; websiteId: string; - eventName: string; propertyName: string; - eventFilters?: EventPropertyFilter[]; + propertyFilters?: PropertyFilter[]; + eventName?: string; }) { const { theme } = useTheme(); const { t, labels } = useMessages(); const { locale, dateLocale } = useLocale(); const { timezone } = useTimezone(); const { colors } = useMemo(() => getThemeColors(theme), [theme]); - const query = useEventDataDateSeriesQuery(websiteId, eventName, propertyName, eventFilters); + const query = usePropertyDateSeriesQuery(source, websiteId, propertyName, propertyFilters, eventName); const rows = useMemo(() => query.data ?? [], [query.data]); const total = useMemo(() => rows.reduce((sum, { y }) => sum + y, 0), [rows]); const zonedNow = useMemo(() => toZonedTime(new Date(), timezone), [timezone]); - const exactDateRows = useMemo(() => { - return rows - .map(row => { - const date = parseDateValue(row.t); - - return date ? { ...row, date } : null; - }) - .filter((row): row is ParsedDateRow => row !== null) - .sort((a, b) => a.date.getTime() - b.date.getTime()); - }, [rows]); + const exactDateRows = useMemo( + () => + rows + .map(row => { + const date = parseDateValue(row.t); + return date ? { ...row, date } : null; + }) + .filter((row): row is ParsedDateRow => row !== null) + .sort((a, b) => a.date.getTime() - b.date.getTime()), + [rows], + ); const minPropertyDate = exactDateRows[0]?.date ?? null; - const lastExactDateRow = exactDateRows[exactDateRows.length - 1]; - const maxPropertyDate = lastExactDateRow?.date ?? null; - + const maxPropertyDate = exactDateRows[exactDateRows.length - 1]?.date ?? null; const chartUnit = useMemo( - () => - minPropertyDate && maxPropertyDate - ? getChartUnit(minPropertyDate, maxPropertyDate) - : 'day', - [minPropertyDate, maxPropertyDate], + () => (minPropertyDate && maxPropertyDate ? getChartUnit(minPropertyDate, maxPropertyDate) : 'day'), + [maxPropertyDate, minPropertyDate], ); const chartRows = useMemo(() => { if (!exactDateRows.length) return []; - const buckets = exactDateRows.reduce( (obj, row) => { const bucketStart = getBucketStart(row.date, chartUnit, dateLocale); const bucketKey = format(bucketStart, "yyyy-MM-dd'T'HH:mm:ss"); - const existing = obj[bucketKey]; - obj[bucketKey] = { t: bucketKey, - y: (existing?.y ?? 0) + row.y, + y: (obj[bucketKey]?.y ?? 0) + row.y, }; - return obj; }, {} as Record, ); - return Object.values(buckets).sort( (a, b) => (parseDateValue(a.t)?.getTime() ?? 0) - (parseDateValue(b.t)?.getTime() ?? 0), ); - }, [exactDateRows, chartUnit, dateLocale]); + }, [chartUnit, dateLocale, exactDateRows]); const chartData: any = useMemo(() => { if (!minPropertyDate || !maxPropertyDate) { @@ -202,28 +176,14 @@ export function EventDataDateChart({ }, ], }; - }, [ - rows, - minPropertyDate, - maxPropertyDate, - chartRows, - chartUnit, - dateLocale, - colors, - t, - labels.count, - ]); + }, [chartRows, chartUnit, colors, dateLocale, labels.count, maxPropertyDate, minPropertyDate, t]); const renderXLabel = useCallback(renderDateLabels(chartUnit, locale), [chartUnit, locale]); const weekdayTableData = useMemo(() => { if (!exactDateRows.length || total === 0) return []; - const weekStartsOn = dateLocale.options?.weekStartsOn ?? 0; - const weekdayOrder = Array.from( - { length: 7 }, - (_, index) => (weekStartsOn + index) % 7, - ); + const weekdayOrder = Array.from({ length: 7 }, (_, index) => (weekStartsOn + index) % 7); const weekdayTotals = exactDateRows.reduce( (obj, { date, y }) => { const day = date.getDay(); @@ -243,51 +203,35 @@ export function EventDataDateChart({ percent: total ? 100 * (count / total) : 0, }; }); - }, [exactDateRows, total, dateLocale]); + }, [dateLocale, exactDateRows, total]); + const topDateByDay = useMemo(() => { if (!exactDateRows.length || total === 0) return null; - const dayTotals = exactDateRows.reduce( (obj, { date, y }) => { const dayDate = startOfDay(date); const key = format(dayDate, 'yyyy-MM-dd'); - const existing = obj[key]; - - obj[key] = { - date: dayDate, - count: (existing?.count ?? 0) + y, - }; - + obj[key] = { date: dayDate, count: (obj[key]?.count ?? 0) + y }; return obj; }, {} as Record, ); - - return Object.values(dayTotals) - .sort((a, b) => b.count - a.count || b.date.getTime() - a.date.getTime())[0]; + return Object.values(dayTotals).sort((a, b) => b.count - a.count || b.date.getTime() - a.date.getTime())[0]; }, [exactDateRows, total]); - const topWeekday = useMemo( - () => [...weekdayTableData].sort((a, b) => b.count - a.count)[0], - [weekdayTableData], - ); - const weekdayLabels = useMemo( - () => weekdayTableData.map(({ label }) => label), - [weekdayTableData], - ); - const weekdayColorMap = useMemo(() => { - return weekdayLabels.reduce( - (obj, label, index) => { - const color = colord(CHART_COLORS[index % CHART_COLORS.length]); - obj[label] = color.alpha(0.7).toRgbString(); + const topWeekday = useMemo(() => [...weekdayTableData].sort((a, b) => b.count - a.count)[0], [weekdayTableData]); + const weekdayLabels = useMemo(() => weekdayTableData.map(({ label }) => label), [weekdayTableData]); + const weekdayColorMap = useMemo( + () => + weekdayLabels.reduce((obj, label, index) => { + obj[label] = CHART_COLORS[index % CHART_COLORS.length]; return obj; - }, - {} as Record, - ); - }, [weekdayLabels]); + }, {} as Record), + [weekdayLabels], + ); + const weekdayPieChartData = useMemo(() => { if (!weekdayTableData.length) return null; - return { labels: weekdayLabels, datasets: [ @@ -298,86 +242,46 @@ export function EventDataDateChart({ }, ], }; - }, [weekdayTableData, weekdayLabels, weekdayColorMap]); + }, [weekdayColorMap, weekdayLabels, weekdayTableData]); + return ( - - - - - - - - - - + + + + + + + + - - - {chartData && ( - formatLongNumber(Number(value))} - height="400px" - /> - )} - + + {chartData && ( + + )} - - - - - {weekdayPieChartData && ( - - )} - + + {weekdayPieChartData && } diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx b/src/components/property-data/PropertyFilterBar.tsx similarity index 90% rename from src/app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx rename to src/components/property-data/PropertyFilterBar.tsx index 62fe1d39a..4fa27eac3 100644 --- a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx +++ b/src/components/property-data/PropertyFilterBar.tsx @@ -1,16 +1,16 @@ 'use client'; +import { Button, Icon, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen'; import { useMessages } from '@/components/hooks'; import { X } from '@/components/icons'; import { OPERATORS } from '@/lib/constants'; -import type { EventPropertyFilter } from '@/lib/types'; -import { Button, Icon, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen'; +import type { PropertyFilter } from '@/lib/types'; -export function EventDataFilterBar({ +export function PropertyFilterBar({ filters, onChange, }: { - filters: EventPropertyFilter[]; - onChange: (filters: EventPropertyFilter[]) => void; + filters: PropertyFilter[]; + onChange: (filters: PropertyFilter[]) => void; }) { const { t, labels } = useMessages(); @@ -47,10 +47,6 @@ export function EventDataFilterBar({ } }; - const handleRemove = (index: number) => { - onChange(filters.filter((_, i) => i !== index)); - }; - return ( handleRemove(index)} + onRemove={() => onChange(filters.filter((_, i) => i !== index))} /> ))} diff --git a/src/components/property-data/PropertyFilterButton.tsx b/src/components/property-data/PropertyFilterButton.tsx new file mode 100644 index 000000000..b5beb3de6 --- /dev/null +++ b/src/components/property-data/PropertyFilterButton.tsx @@ -0,0 +1,41 @@ +'use client'; +import { useMessages } from '@/components/hooks'; +import { ListFilter } from '@/components/icons'; +import { DialogButton } from '@/components/input/DialogButton'; +import type { PropertyFilter } from '@/lib/types'; +import type { PropertyDataSource } from '@/components/hooks/queries/usePropertyFieldsQuery'; +import { PropertyFilterEditForm } from './PropertyFilterEditForm'; + +export function PropertyFilterButton({ + source, + websiteId, + eventName, + fields, + filters, + onApply, +}: { + source: PropertyDataSource; + websiteId: string; + eventName?: string; + fields?: Array<{ propertyName: string; dataType: number }>; + filters: PropertyFilter[]; + onApply: (filters: PropertyFilter[]) => void; +}) { + const { t, labels } = useMessages(); + + return ( + } label={t(labels.propertyFilter)} variant="outline"> + {({ close }) => ( + + )} + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterEditForm.tsx b/src/components/property-data/PropertyFilterEditForm.tsx similarity index 67% rename from src/app/(main)/websites/[websiteId]/event-data/EventDataFilterEditForm.tsx rename to src/components/property-data/PropertyFilterEditForm.tsx index ab394b0d0..6d2db0b5f 100644 --- a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterEditForm.tsx +++ b/src/components/property-data/PropertyFilterEditForm.tsx @@ -3,34 +3,41 @@ import { Button, Column, Grid, Icon, List, ListItem, Menu, MenuItem, MenuTrigger import { format } from 'date-fns'; import { useState } from 'react'; import { Empty } from '@/components/common/Empty'; -import { useEventDataFieldsQuery, useMessages, useMobile } from '@/components/hooks'; +import { useMessages, useMobile, usePropertyFieldsQuery } from '@/components/hooks'; import { Plus } from '@/components/icons'; import { DATA_TYPE, OPERATORS } from '@/lib/constants'; -import type { EventPropertyFilter } from '@/lib/types'; -import { EventDataFilterRecord } from './EventDataFilterRecord'; +import type { PropertyFilter } from '@/lib/types'; +import type { PropertyDataSource } from '@/components/hooks/queries/usePropertyFieldsQuery'; +import { PropertyFilterRecord } from './PropertyFilterRecord'; -export function EventDataFilterEditForm({ +export function PropertyFilterEditForm({ + source, websiteId, eventName, + fields: providedFields, value, onApply, onClose, }: { + source: PropertyDataSource; websiteId: string; - eventName: string; - value: EventPropertyFilter[]; - onApply: (filters: EventPropertyFilter[]) => void; + eventName?: string; + fields?: Array<{ propertyName: string; dataType: number }>; + value: PropertyFilter[]; + onApply: (filters: PropertyFilter[]) => void; onClose: () => void; }) { const { t, labels, messages } = useMessages(); const { isMobile } = useMobile(); - const [filters, setFilters] = useState(value); - - const { data: fields = [] } = useEventDataFieldsQuery(websiteId, eventName); + const [filters, setFilters] = useState(value); + const { data: queriedFields = [] } = usePropertyFieldsQuery(source, websiteId, eventName, { + enabled: !providedFields, + }); + const fields = providedFields ?? queriedFields; const handleAdd = (propertyName: string) => { const field = (fields as any[]).find(f => f.propertyName === propertyName); - const dataType: number = field?.dataType ?? 1; + const dataType: number = field?.dataType ?? DATA_TYPE.string; setFilters(prev => [ ...prev, { @@ -52,14 +59,6 @@ export function EventDataFilterEditForm({ ]); }; - const handleChange = (index: number, filter: EventPropertyFilter) => { - setFilters(prev => prev.map((f, i) => (i === index ? filter : f))); - }; - - const handleRemove = (index: number) => { - setFilters(prev => prev.filter((_, i) => i !== index)); - }; - return ( @@ -77,7 +76,7 @@ export function EventDataFilterEditForm({ style={{ maxHeight: 'calc(100vh - 2rem)', overflowY: 'auto' }} > {(fields as any[]).map(field => ( - + {field.propertyName} ))} @@ -93,7 +92,7 @@ export function EventDataFilterEditForm({ > handleAdd(key.toString())}> {(fields as any[]).map(field => ( - + {field.propertyName} ))} @@ -101,13 +100,15 @@ export function EventDataFilterEditForm({ {filters.map((filter, index) => ( - handleChange(index, f)} - onRemove={() => handleRemove(index)} + filters={filters} + onChange={f => setFilters(prev => prev.map((item, i) => (i === index ? f : item)))} + onRemove={() => setFilters(prev => prev.filter((_, i) => i !== index))} /> ))} {!filters.length && } @@ -118,7 +119,13 @@ export function EventDataFilterEditForm({ - diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterRecord.tsx b/src/components/property-data/PropertyFilterRecord.tsx similarity index 89% rename from src/app/(main)/websites/[websiteId]/event-data/EventDataFilterRecord.tsx rename to src/components/property-data/PropertyFilterRecord.tsx index e6d58027a..9a36df9f4 100644 --- a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterRecord.tsx +++ b/src/components/property-data/PropertyFilterRecord.tsx @@ -5,11 +5,12 @@ import { useState } from 'react'; import { DateDisplay } from '@/components/common/DateDisplay'; import { Empty } from '@/components/common/Empty'; import { MultiSelect } from '@/components/common/MultiSelect'; -import { useEventDataValuesQuery, useMessages } from '@/components/hooks'; +import { useMessages, usePropertyValuesQuery } from '@/components/hooks'; import { X } from '@/components/icons'; import { DATA_TYPE, OPERATORS } from '@/lib/constants'; import { getMaxSelectableDate } from '@/lib/date'; -import type { EventPropertyFilter, Operator } from '@/lib/types'; +import type { Operator, PropertyFilter } from '@/lib/types'; +import type { PropertyDataSource } from '@/components/hooks/queries/usePropertyFieldsQuery'; const STRING_OPERATORS: Operator[] = [ OPERATORS.equals, @@ -38,17 +39,21 @@ const FREE_TEXT_OPERATORS: Operator[] = [ OPERATORS.notRegex, ]; -export function EventDataFilterRecord({ +export function PropertyFilterRecord({ + source, websiteId, eventName, filter, + filters, onChange, onRemove, }: { + source: PropertyDataSource; websiteId: string; - eventName: string; - filter: EventPropertyFilter; - onChange: (filter: EventPropertyFilter) => void; + eventName?: string; + filter: PropertyFilter; + filters: PropertyFilter[]; + onChange: (filter: PropertyFilter) => void; onRemove: () => void; }) { const { t, labels, messages } = useMessages(); @@ -69,11 +74,13 @@ export function EventDataFilterRecord({ : STRING_OPERATORS; const isFreeText = !isArray && FREE_TEXT_OPERATORS.includes(filter.operator); - const { data, isLoading } = useEventDataValuesQuery( + const { data, isLoading } = usePropertyValuesQuery( + source, websiteId, - eventName, filter.propertyName, filter.dataType, + filters.filter(f => f !== filter), + eventName, { enabled: !isNumeric && !isBoolean && !isDate && !isFreeText }, ); @@ -102,7 +109,6 @@ export function EventDataFilterRecord({ }; const handleOperatorChange = (op: Operator) => { - // clear value when switching between multi-select and free-text modes const wasMulti = MULTI_OPERATORS.includes(filter.operator); const isMulti = MULTI_OPERATORS.includes(op); onChange({ @@ -218,19 +224,15 @@ function DateValuePicker({ const selectedDate = getSelectedDate(value); const [draftDate, setDraftDate] = useState(selectedDate); - const handleOpen = () => { - setDraftDate(getSelectedDate(value)); - setShowPicker(true); - }; - - const handleApply = () => { - onChange(format(draftDate, 'yyyy-MM-dd')); - setShowPicker(false); - }; - return ( - @@ -244,7 +246,13 @@ function DateValuePicker({ /> - diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataNumericChart.tsx b/src/components/property-data/PropertyNumericChart.tsx similarity index 65% rename from src/app/(main)/websites/[websiteId]/event-data/EventDataNumericChart.tsx rename to src/components/property-data/PropertyNumericChart.tsx index 94da88f82..ed98e23df 100644 --- a/src/app/(main)/websites/[websiteId]/event-data/EventDataNumericChart.tsx +++ b/src/components/property-data/PropertyNumericChart.tsx @@ -5,30 +5,33 @@ import { BarChart } from '@/components/charts/BarChart'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { useDateRange, - useEventDataNumericSeriesQuery, - useEventDataNumericStatsQuery, useLocale, useMessages, + usePropertyNumericSeriesQuery, + usePropertyNumericStatsQuery, useTimezone, } from '@/components/hooks'; +import type { PropertyDataSource } from '@/components/hooks/queries/usePropertyFieldsQuery'; import { MetricCard } from '@/components/metrics/MetricCard'; import { MetricsBar } from '@/components/metrics/MetricsBar'; import { renderDateLabels } from '@/lib/charts'; import { getThemeColors } from '@/lib/colors'; import { generateTimeSeries } from '@/lib/date'; import { formatLongNumber } from '@/lib/format'; -import type { EventPropertyFilter } from '@/lib/types'; +import type { PropertyFilter } from '@/lib/types'; -export function EventDataNumericChart({ +export function PropertyNumericChart({ + source, websiteId, - eventName, propertyName, - eventFilters = [], + propertyFilters = [], + eventName, }: { + source: PropertyDataSource; websiteId: string; - eventName: string; propertyName: string; - eventFilters?: EventPropertyFilter[]; + propertyFilters?: PropertyFilter[]; + eventName?: string; }) { const { t, labels } = useMessages(); const { theme } = useTheme(); @@ -37,35 +40,12 @@ export function EventDataNumericChart({ const { locale, dateLocale } = useLocale(); const { colors } = useMemo(() => getThemeColors(theme), [theme]); - const sumQuery = useEventDataNumericSeriesQuery( - websiteId, - eventName, - propertyName, - 'sum', - eventFilters, - ); - const avgQuery = useEventDataNumericSeriesQuery( - websiteId, - eventName, - propertyName, - 'avg', - eventFilters, - ); - const statsQuery = useEventDataNumericStatsQuery( - websiteId, - eventName, - propertyName, - eventFilters, - ); + const sumQuery = usePropertyNumericSeriesQuery(source, websiteId, propertyName, 'sum', propertyFilters, eventName); + const avgQuery = usePropertyNumericSeriesQuery(source, websiteId, propertyName, 'avg', propertyFilters, eventName); + const statsQuery = usePropertyNumericStatsQuery(source, websiteId, propertyName, propertyFilters, eventName); - const sumRows = useMemo( - () => (sumQuery.data as { t: string; y: number }[] | undefined) ?? [], - [sumQuery.data], - ); - const avgRows = useMemo( - () => (avgQuery.data as { t: string; y: number }[] | undefined) ?? [], - [avgQuery.data], - ); + const sumRows = useMemo(() => (sumQuery.data as { t: string; y: number }[] | undefined) ?? [], [sumQuery.data]); + const avgRows = useMemo(() => (avgQuery.data as { t: string; y: number }[] | undefined) ?? [], [avgQuery.data]); const stats = statsQuery.data; const formatMetricValue = useCallback( @@ -114,7 +94,7 @@ export function EventDataNumericChart({ }, ], }; - }, [sumQuery.data, avgQuery.data, t, labels, startDate, endDate, unit, dateLocale, colors, sumRows, avgRows]); + }, [avgQuery.data, avgRows, colors, dateLocale, endDate, labels.average, labels.sum, startDate, sumQuery.data, sumRows, t, unit]); const renderXLabel = useCallback(renderDateLabels(unit, locale), [unit, locale]); @@ -128,31 +108,11 @@ export function EventDataNumericChart({ minHeight="100px" > - - - - - + + + + + , options?: QueryOptions) { }; } -function getEventPropertyFilterQuery( - filters: EventPropertyFilter[] = [], +function getPropertyFilterQuery( + filters: PropertyFilter[] = [], + propertyType: 'event' | 'session' = 'event', timezone?: string, ): { sql: string; @@ -249,10 +250,13 @@ function getEventPropertyFilterQuery( const parts: string[] = []; const params: Record = {}; + const table = propertyType === 'event' ? 'event_data' : 'session_data final'; + const column = propertyType === 'event' ? 'event_id' : 'session_id'; + const outerColumn = propertyType === 'event' ? 'event_id' : 'website_event.session_id'; filters.forEach(({ propertyName, dataType, operator, value }, i) => { - const keyParam = `epf_key_${i}`; - const valParam = `epf_val_${i}`; + const keyParam = `pf_key_${i}`; + const valParam = `pf_val_${i}`; params[keyParam] = propertyName; let condition: string; @@ -329,9 +333,9 @@ function getEventPropertyFilterQuery( } } - parts.push(`and event_id in ( - select event_id - from event_data + parts.push(`and ${outerColumn} in ( + select ${column} + from ${table} where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} and data_key = {${keyParam}:String} @@ -429,7 +433,7 @@ export default { getDateSQL, getSearchSQL, getFilterQuery, - getEventPropertyFilterQuery, + getPropertyFilterQuery, getUTCString, parseFilters, pagedRawQuery, diff --git a/src/lib/params.ts b/src/lib/params.ts index 6f430ddbe..50cb6db2d 100644 --- a/src/lib/params.ts +++ b/src/lib/params.ts @@ -1,5 +1,5 @@ import { DATA_TYPE, FILTER_COLUMNS, OPERATORS } from '@/lib/constants'; -import type { EventPropertyFilter, Filter, Operator, QueryFilters, QueryOptions } from '@/lib/types'; +import type { Filter, Operator, PropertyFilter, QueryFilters, QueryOptions } from '@/lib/types'; const VALID_OPERATORS: Operator[] = Object.values(OPERATORS); const VALID_EVENT_DATA_TYPES = Object.values(DATA_TYPE); @@ -100,12 +100,24 @@ export function filtersArrayToObject(filters: Filter[]) { }, {}); } -export function parseEventPropertyFilters(query: Record): EventPropertyFilter[] { +function getPropertyFilterPrefix(key: string, prefixes: string[]) { + return prefixes.find(prefix => key.startsWith(`${prefix}_`)); +} + +export function parsePropertyFilters( + query: Record, + prefixes: string[] = ['pf'], +): PropertyFilter[] { return Object.entries(query) - .filter(([key]) => /^epf_/.test(key)) .flatMap(([key, val]) => { + const prefix = getPropertyFilterPrefix(key, prefixes); + + if (!prefix) { + return []; + } + const stringValue = String(val); - const withoutPrefix = key.slice(4); // strip "epf_" + const withoutPrefix = key.slice(prefix.length + 1); const propertyName = withoutPrefix.replace(/\d+$/, ''); // strip trailing index digits const prefixedDotMatch = stringValue.match(/^(\d+)\.([^.]+)\.(.*)$/); const untypedDotMatch = stringValue.match(/^([^.]+)\.(.*)$/); @@ -134,13 +146,24 @@ export function parseEventPropertyFilters(query: Record): EventProp }); } -export function serializeEventPropertyFilters(filters: EventPropertyFilter[]): Record { +export function parseEventPropertyFilters(query: Record) { + return parsePropertyFilters(query, ['pf']); +} + +export function serializePropertyFilters( + filters: PropertyFilter[], + prefix = 'pf', +): Record { const counts: Record = {}; return Object.fromEntries( filters.map(f => { const n = counts[f.propertyName] ?? 0; counts[f.propertyName] = n + 1; - return [`epf_${f.propertyName}${n > 0 ? n : ''}`, `${f.dataType}.${f.operator}.${f.value}`]; + return [`${prefix}_${f.propertyName}${n > 0 ? n : ''}`, `${f.dataType}.${f.operator}.${f.value}`]; }), ); } + +export function serializeEventPropertyFilters(filters: PropertyFilter[]) { + return serializePropertyFilters(filters, 'pf'); +} diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index baab7c2a4..1da0f81b7 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -4,7 +4,7 @@ import { readReplicas } from '@prisma/extension-read-replicas'; import debug from 'debug'; import { DATA_TYPE, DEFAULT_PAGE_SIZE, FILTER_COLUMNS, OPERATORS, SESSION_COLUMNS } from './constants'; import { filtersObjectToArray } from './params'; -import type { EventPropertyFilter, Operator, QueryFilters, QueryOptions } from './types'; +import type { Operator, PropertyFilter, QueryFilters, QueryOptions } from './types'; const log = debug('umami:prisma'); @@ -273,8 +273,9 @@ function parseFilters(filters: Record, options?: QueryOptions) { }; } -function getEventPropertyFilterQuery( - filters: EventPropertyFilter[] = [], +function getPropertyFilterQuery( + filters: PropertyFilter[] = [], + propertyType: 'event' | 'session' = 'event', timezone?: string, ): { sql: string; @@ -284,10 +285,14 @@ function getEventPropertyFilterQuery( const parts: string[] = []; const params: Record = {}; + const table = propertyType === 'event' ? 'event_data' : 'session_data'; + const column = propertyType === 'event' ? 'website_event_id' : 'session_id'; + const outerColumn = + propertyType === 'event' ? 'website_event.event_id' : 'website_event.session_id'; filters.forEach(({ propertyName, dataType, operator, value }, i) => { - const keyParam = `epf_key_${i}`; - const valParam = `epf_val_${i}`; + const keyParam = `pf_key_${i}`; + const valParam = `pf_val_${i}`; params[keyParam] = propertyName; let condition: string; @@ -346,8 +351,8 @@ function getEventPropertyFilterQuery( params[valParam] = vals; condition = operator === OPERATORS.equals - ? `${col} = ANY({{${valParam}::text[]}})` - : `${col} != ALL({{${valParam}::text[]}})`; + ? `${col} = ANY({{${valParam}}})` + : `${col} != ALL({{${valParam}}})`; } else if (REGEX_OPERATORS.includes(operator)) { if (!value) return; params[valParam] = value; @@ -367,9 +372,9 @@ function getEventPropertyFilterQuery( } } - parts.push(`and website_event.event_id in ( - select website_event_id - from event_data + parts.push(`and ${outerColumn} in ( + select ${column} + from ${table} where website_id = {{websiteId::uuid}} and created_at between {{startDate}} and {{endDate}} and data_key = {{${keyParam}}} @@ -567,7 +572,7 @@ export default { getDateStringSQL, getDateWeeklySQL, getFilterQuery, - getEventPropertyFilterQuery, + getPropertyFilterQuery, getSearchParameters, getTimestampDiffSQL, getSearchSQL, diff --git a/src/lib/request.ts b/src/lib/request.ts index 40ffabe06..9b32b6736 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -32,7 +32,7 @@ export async function parseRequest( // Re-add dynamic params stripped by Zod schema: suffixed filter params (browser1, os2) for (const key of Object.keys(rawQuery)) { - if ((/\d+$/.test(key) || /^epf_/.test(key)) && !(key in query)) { + if ((/\d+$/.test(key) || /^pf_/.test(key)) && !(key in query)) { query[key] = rawQuery[key]; } } diff --git a/src/lib/types.ts b/src/lib/types.ts index c93854910..587f77b96 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -30,13 +30,15 @@ export interface Auth { }; } -export interface EventPropertyFilter { +export interface PropertyFilter { propertyName: string; dataType: number; operator: Operator; value: string; } +export type EventPropertyFilter = PropertyFilter; + export interface Filter { name: string; operator: Operator; @@ -79,6 +81,31 @@ export interface EventDataNumericStats { min: number; } +export interface SessionDataPivotRow { + sessionId: string; + distinctId: string; + createdAt: string | Date; + propertyKeys: string[]; + propertyValues: string[]; +} + +export interface PropertyCardinalityBucket { + label: string; + count: number; +} + +export interface PropertyLeaderboardRow { + label: string; + activity: number; + sessions: number; + visits: number; + views: number; + events: number; + totaltime: number; + newSessions: number; + returningSessions: number; +} + export interface QueryOptions { joinSession?: boolean; columns?: Record; diff --git a/src/queries/sql/events/getEventDataArraySeries.ts b/src/queries/sql/events/getEventDataArraySeries.ts index acf1e2034..6d3e3989c 100644 --- a/src/queries/sql/events/getEventDataArraySeries.ts +++ b/src/queries/sql/events/getEventDataArraySeries.ts @@ -29,13 +29,13 @@ async function relationalQuery( eventFilters: EventPropertyFilter[] = [], ): Promise { const { timezone = 'utc', unit = 'day' } = filters; - const { rawQuery, getDateSQL, parseFilters, getEventPropertyFilterQuery } = prisma; + const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = prisma; const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone, }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); return rawQuery( ` @@ -57,11 +57,11 @@ async function relationalQuery( and event_data.data_key = {{propertyName}} and event_data.data_type = ${DATA_TYPE.array} ${filterQuery} - ${epfSQL} + ${pfSQL} group by 1, 2 order by 2 `, - { ...queryParams, eventName, propertyName, ...epfParams }, + { ...queryParams, eventName, propertyName, ...pfParams }, FUNCTION_NAME, ); } @@ -74,9 +74,9 @@ async function clickhouseQuery( eventFilters: EventPropertyFilter[] = [], ): Promise { const { timezone = 'UTC', unit = 'day' } = filters; - const { rawQuery, getDateSQL, parseFilters, getEventPropertyFilterQuery } = clickhouse; + const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = clickhouse; const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); return rawQuery( ` @@ -101,11 +101,11 @@ async function clickhouseQuery( and event_data.data_key = {propertyName:String} and event_data.data_type = ${DATA_TYPE.array} ${filterQuery} - ${epfSQL} + ${pfSQL} group by x, t order by t `, - { ...queryParams, eventName, propertyName, ...epfParams }, + { ...queryParams, eventName, propertyName, ...pfParams }, FUNCTION_NAME, ); } diff --git a/src/queries/sql/events/getEventDataDateSeries.ts b/src/queries/sql/events/getEventDataDateSeries.ts index 711968926..26719dbc3 100644 --- a/src/queries/sql/events/getEventDataDateSeries.ts +++ b/src/queries/sql/events/getEventDataDateSeries.ts @@ -29,13 +29,13 @@ async function relationalQuery( eventFilters: EventPropertyFilter[] = [], ) : Promise { const { timezone = 'utc' } = filters; - const { rawQuery, parseFilters, getDateStringSQL, getEventPropertyFilterQuery } = prisma; + const { rawQuery, parseFilters, getDateStringSQL, getPropertyFilterQuery } = prisma; const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone, }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); return rawQuery( ` @@ -55,11 +55,11 @@ async function relationalQuery( and event_data.data_key = {{propertyName}} and event_data.data_type = ${DATA_TYPE.date} ${filterQuery} - ${epfSQL} + ${pfSQL} group by 1 order by 1 `, - { ...queryParams, eventName, propertyName, ...epfParams }, + { ...queryParams, eventName, propertyName, ...pfParams }, FUNCTION_NAME, ); } @@ -72,9 +72,9 @@ async function clickhouseQuery( eventFilters: EventPropertyFilter[] = [], ): Promise { const { timezone = 'UTC' } = filters; - const { rawQuery, parseFilters, getDateStringSQL, getEventPropertyFilterQuery } = clickhouse; + const { rawQuery, parseFilters, getDateStringSQL, getPropertyFilterQuery } = clickhouse; const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); return rawQuery( ` @@ -98,11 +98,11 @@ async function clickhouseQuery( and event_data.data_key = {propertyName:String} and event_data.data_type = ${DATA_TYPE.date} ${filterQuery} - ${epfSQL} + ${pfSQL} group by t order by t `, - { ...queryParams, eventName, propertyName, ...epfParams }, + { ...queryParams, eventName, propertyName, ...pfParams }, FUNCTION_NAME, ); } diff --git a/src/queries/sql/events/getEventDataNumericSeries.ts b/src/queries/sql/events/getEventDataNumericSeries.ts index 2624380ba..1fca6c0f6 100644 --- a/src/queries/sql/events/getEventDataNumericSeries.ts +++ b/src/queries/sql/events/getEventDataNumericSeries.ts @@ -30,13 +30,13 @@ async function relationalQuery( eventFilters: EventPropertyFilter[] = [], ) { const { timezone = 'utc', unit = 'day' } = filters; - const { rawQuery, getDateSQL, parseFilters, getEventPropertyFilterQuery } = prisma; + const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = prisma; const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone, }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); const aggSql = metric === 'avg' ? 'avg(cast(event_data.number_value as decimal))' : metric === 'count' ? 'count(*)' : @@ -60,11 +60,11 @@ async function relationalQuery( and event_data.data_key = {{propertyName}} and event_data.data_type = 2 ${filterQuery} - ${epfSQL} + ${pfSQL} group by 1 order by 1 `, - { ...queryParams, eventName, propertyName, ...epfParams }, + { ...queryParams, eventName, propertyName, ...pfParams }, FUNCTION_NAME, ); } @@ -78,9 +78,9 @@ async function clickhouseQuery( eventFilters: EventPropertyFilter[] = [], ): Promise<{ t: string; y: number }[]> { const { timezone = 'UTC', unit = 'day' } = filters; - const { rawQuery, getDateSQL, parseFilters, getEventPropertyFilterQuery } = clickhouse; + const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = clickhouse; const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); const aggSql = metric === 'avg' ? 'avg(event_data.number_value)' : metric === 'count' ? 'count()' : @@ -108,11 +108,11 @@ async function clickhouseQuery( and event_data.data_key = {propertyName:String} and event_data.data_type = 2 ${filterQuery} - ${epfSQL} + ${pfSQL} group by t order by t `, - { ...queryParams, eventName, propertyName, ...epfParams }, + { ...queryParams, eventName, propertyName, ...pfParams }, FUNCTION_NAME, ); } diff --git a/src/queries/sql/events/getEventDataNumericStats.ts b/src/queries/sql/events/getEventDataNumericStats.ts index 64a3f884b..90896d989 100644 --- a/src/queries/sql/events/getEventDataNumericStats.ts +++ b/src/queries/sql/events/getEventDataNumericStats.ts @@ -28,13 +28,13 @@ async function relationalQuery( eventFilters: EventPropertyFilter[] = [], ) { const { timezone = 'utc' } = filters; - const { rawQuery, parseFilters, getEventPropertyFilterQuery } = prisma; + const { rawQuery, parseFilters, getPropertyFilterQuery } = prisma; const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone, }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); return rawQuery( ` @@ -57,9 +57,9 @@ async function relationalQuery( and event_data.data_key = {{propertyName}} and event_data.data_type = 2 ${filterQuery} - ${epfSQL} + ${pfSQL} `, - { ...queryParams, eventName, propertyName, ...epfParams }, + { ...queryParams, eventName, propertyName, ...pfParams }, FUNCTION_NAME, ); } @@ -72,9 +72,9 @@ async function clickhouseQuery( eventFilters: EventPropertyFilter[] = [], ): Promise { const { timezone = 'UTC' } = filters; - const { rawQuery, parseFilters, getEventPropertyFilterQuery } = clickhouse; + const { rawQuery, parseFilters, getPropertyFilterQuery } = clickhouse; const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); return rawQuery( ` @@ -101,9 +101,9 @@ async function clickhouseQuery( and event_data.data_key = {propertyName:String} and event_data.data_type = 2 ${filterQuery} - ${epfSQL} + ${pfSQL} `, - { ...queryParams, eventName, propertyName, ...epfParams }, + { ...queryParams, eventName, propertyName, ...pfParams }, FUNCTION_NAME, ); } diff --git a/src/queries/sql/events/getEventDataPivot.ts b/src/queries/sql/events/getEventDataPivot.ts index 18f8aabda..7a494a1da 100644 --- a/src/queries/sql/events/getEventDataPivot.ts +++ b/src/queries/sql/events/getEventDataPivot.ts @@ -22,7 +22,7 @@ export async function getEventDataPivot( async function relationalQuery(websiteId: string, eventName: string, filters: QueryFilters, eventFilters: EventPropertyFilter[] = []) { const { timezone = 'utc' } = filters; - const { rawQuery, parseFilters, getEventPropertyFilterQuery } = prisma; + const { rawQuery, parseFilters, getPropertyFilterQuery } = prisma; const { page = 1, pageSize } = filters; const size = +pageSize || DEFAULT_PAGE_SIZE; const offset = +size * (+page - 1); @@ -32,7 +32,7 @@ async function relationalQuery(websiteId: string, eventName: string, filters: Qu websiteId, timezone, }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); const countResult = await rawQuery( ` @@ -47,9 +47,9 @@ async function relationalQuery(websiteId: string, eventName: string, filters: Qu and website_event.created_at between {{startDate}} and {{endDate}} and website_event.event_name = {{eventName}} ${filterQuery} - ${epfSQL} + ${pfSQL} `, - { ...queryParams, eventName, ...epfParams }, + { ...queryParams, eventName, ...pfParams }, ); const count = countResult[0].num; @@ -68,7 +68,7 @@ async function relationalQuery(websiteId: string, eventName: string, filters: Qu and website_event.created_at between {{startDate}} and {{endDate}} and website_event.event_name = {{eventName}} ${filterQuery} - ${epfSQL} + ${pfSQL} group by website_event.event_id order by max(website_event.created_at) desc limit ${size} offset ${offset} @@ -97,7 +97,7 @@ async function relationalQuery(websiteId: string, eventName: string, filters: Qu and event_data.created_at between {{startDate}} and {{endDate}} order by website_event.created_at desc `, - { ...queryParams, eventName, ...epfParams }, + { ...queryParams, eventName, ...pfParams }, FUNCTION_NAME, ); @@ -120,13 +120,13 @@ async function relationalQuery(websiteId: string, eventName: string, filters: Qu async function clickhouseQuery(websiteId: string, eventName: string, filters: QueryFilters, eventFilters: EventPropertyFilter[] = []) { const { timezone = 'UTC' } = filters; - const { rawQuery, parseFilters, getEventPropertyFilterQuery } = clickhouse; + const { rawQuery, parseFilters, getPropertyFilterQuery } = clickhouse; const { page = 1, pageSize } = filters; const size = +pageSize || DEFAULT_PAGE_SIZE; const offset = +size * (+page - 1); const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); const count = await rawQuery( ` @@ -146,9 +146,9 @@ async function clickhouseQuery(websiteId: string, eventName: string, filters: Qu where event_data_pivot.website_id = {websiteId:UUID} and event_data_pivot.created_at between {startDate:DateTime64} and {endDate:DateTime64} ${filterQuery} - ${epfSQL} + ${pfSQL} `, - { ...queryParams, eventName, ...epfParams }, + { ...queryParams, eventName, ...pfParams }, ).then((res: any) => res[0].num); const data = await rawQuery( @@ -176,7 +176,7 @@ async function clickhouseQuery(websiteId: string, eventName: string, filters: Qu where event_data_pivot.website_id = {websiteId:UUID} and event_data_pivot.created_at between {startDate:DateTime64} and {endDate:DateTime64} ${filterQuery} - ${epfSQL} + ${pfSQL} group by event_data_pivot.event_id, event_data_pivot.session_id, @@ -186,7 +186,7 @@ async function clickhouseQuery(websiteId: string, eventName: string, filters: Qu order by event_data_pivot.created_at desc limit ${size} offset ${offset} `, - { ...queryParams, eventName, ...epfParams }, + { ...queryParams, eventName, ...pfParams }, FUNCTION_NAME, ); diff --git a/src/queries/sql/events/getEventDataPropertySeries.ts b/src/queries/sql/events/getEventDataPropertySeries.ts index e48045236..b2de7d379 100644 --- a/src/queries/sql/events/getEventDataPropertySeries.ts +++ b/src/queries/sql/events/getEventDataPropertySeries.ts @@ -29,13 +29,13 @@ async function relationalQuery( eventFilters: EventPropertyFilter[] = [], ): Promise { const { timezone = 'utc', unit = 'day' } = filters; - const { rawQuery, getDateSQL, parseFilters, getEventPropertyFilterQuery } = prisma; + const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = prisma; const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone, }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); return rawQuery( ` @@ -56,11 +56,11 @@ async function relationalQuery( and event_data.data_key = {{propertyName}} and event_data.data_type in (${DATA_TYPE.string}, ${DATA_TYPE.boolean}) ${filterQuery} - ${epfSQL} + ${pfSQL} group by 1, 2 order by 2 `, - { ...queryParams, eventName, propertyName, ...epfParams }, + { ...queryParams, eventName, propertyName, ...pfParams }, FUNCTION_NAME, ); } @@ -73,9 +73,9 @@ async function clickhouseQuery( eventFilters: EventPropertyFilter[] = [], ): Promise { const { timezone = 'UTC', unit = 'day' } = filters; - const { rawQuery, getDateSQL, parseFilters, getEventPropertyFilterQuery } = clickhouse; + const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = clickhouse; const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); - const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters, timezone); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); return rawQuery( ` @@ -100,11 +100,11 @@ async function clickhouseQuery( and event_data.data_key = {propertyName:String} and event_data.data_type in (${DATA_TYPE.string}, ${DATA_TYPE.boolean}) ${filterQuery} - ${epfSQL} + ${pfSQL} group by x, t order by t `, - { ...queryParams, eventName, propertyName, ...epfParams }, + { ...queryParams, eventName, propertyName, ...pfParams }, FUNCTION_NAME, ); } diff --git a/src/queries/sql/events/getEventDataValues.ts b/src/queries/sql/events/getEventDataValues.ts index 5063646b7..8196ff5ce 100644 --- a/src/queries/sql/events/getEventDataValues.ts +++ b/src/queries/sql/events/getEventDataValues.ts @@ -76,6 +76,7 @@ async function relationalQuery( where event_data.website_id = {{websiteId::uuid}} and event_data.created_at between {{startDate}} and {{endDate}} and event_data.data_key = {{propertyName}} + ${dataType ? `and event_data.data_type = ${dataType}` : ''} ${filterQuery} group by value order by 2 desc @@ -146,6 +147,7 @@ async function clickhouseQuery( where event_data.website_id = {websiteId:UUID} and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} and event_data.data_key = {propertyName:String} + ${dataType ? `and event_data.data_type = ${dataType}` : ''} ${filterQuery} group by value order by 2 desc diff --git a/src/queries/sql/index.ts b/src/queries/sql/index.ts index f5a05d758..13800ef3e 100644 --- a/src/queries/sql/index.ts +++ b/src/queries/sql/index.ts @@ -41,7 +41,14 @@ export * from './reports/getUTM'; export * from './sessions/createSession'; export * from './sessions/getSessionActivity'; export * from './sessions/getSessionData'; +export * from './sessions/getSessionDataArraySeries'; +export * from './sessions/getSessionDataDateSeries'; export * from './sessions/getSessionDataProperties'; +export * from './sessions/getSessionDataPropertySeries'; +export * from './sessions/getSessionDataNumericSeries'; +export * from './sessions/getSessionDataNumericStats'; +export * from './sessions/getSessionDataPivot'; +export * from './sessions/getSessionDataStats'; export * from './sessions/getSessionDataValues'; export * from './sessions/getSessionExpandedMetrics'; export * from './sessions/getSessionMetrics'; diff --git a/src/queries/sql/sessions/getSessionDataArraySeries.ts b/src/queries/sql/sessions/getSessionDataArraySeries.ts new file mode 100644 index 000000000..a4b81ecb1 --- /dev/null +++ b/src/queries/sql/sessions/getSessionDataArraySeries.ts @@ -0,0 +1,99 @@ +import clickhouse from '@/lib/clickhouse'; +import { DATA_TYPE } from '@/lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import type { EventDataSeriesPoint, PropertyFilter, QueryFilters } from '@/lib/types'; + +const FUNCTION_NAME = 'getSessionDataArraySeries'; + +export async function getSessionDataArraySeries( + ...args: [ + websiteId: string, + propertyName: string, + filters: QueryFilters, + propertyFilters?: PropertyFilter[], + ] +): Promise { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery( + websiteId: string, + propertyName: string, + filters: QueryFilters, + propertyFilters: PropertyFilter[] = [], +): Promise { + const { timezone = 'utc', unit = 'day' } = filters; + const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = prisma; + const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + timezone, + }); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(propertyFilters, 'session', timezone); + + return rawQuery( + ` + select + array_item.value as x, + ${getDateSQL('session_data.created_at', unit, timezone)} as t, + count(distinct session_data.session_id) as y + from website_event + ${cohortQuery} + ${joinSessionQuery} + join session_data + on session_data.session_id = website_event.session_id + and session_data.website_id = website_event.website_id + cross join lateral jsonb_array_elements_text(coalesce(session_data.string_value, '[]')::jsonb) as array_item(value) + where website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + and session_data.data_key = {{propertyName}} + and session_data.data_type = ${DATA_TYPE.array} + ${filterQuery} + ${pfSQL} + group by 1, 2 + order by 2 + `, + { ...queryParams, propertyName, ...pfParams }, + FUNCTION_NAME, + ); +} + +async function clickhouseQuery( + websiteId: string, + propertyName: string, + filters: QueryFilters, + propertyFilters: PropertyFilter[] = [], +): Promise { + const { timezone = 'UTC', unit = 'day' } = filters; + const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = clickhouse; + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(propertyFilters, 'session', timezone); + + return rawQuery( + ` + select + arrayJoin(JSONExtract(ifNull(session_data.string_value, '[]'), 'Array(String)')) as x, + ${getDateSQL('session_data.created_at', unit, timezone)} as t, + uniq(session_data.session_id) as y + from website_event + ${cohortQuery} + join session_data final + on session_data.session_id = website_event.session_id + and session_data.website_id = {websiteId:UUID} + where website_event.website_id = {websiteId:UUID} + and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and session_data.data_key = {propertyName:String} + and session_data.data_type = ${DATA_TYPE.array} + ${filterQuery} + ${pfSQL} + group by x, t + order by t + `, + { ...queryParams, propertyName, ...pfParams }, + FUNCTION_NAME, + ); +} diff --git a/src/queries/sql/sessions/getSessionDataDateSeries.ts b/src/queries/sql/sessions/getSessionDataDateSeries.ts new file mode 100644 index 000000000..d74b5e8a8 --- /dev/null +++ b/src/queries/sql/sessions/getSessionDataDateSeries.ts @@ -0,0 +1,96 @@ +import clickhouse from '@/lib/clickhouse'; +import { DATA_TYPE } from '@/lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import type { EventDataDateSeriesPoint, PropertyFilter, QueryFilters } from '@/lib/types'; + +const FUNCTION_NAME = 'getSessionDataDateSeries'; + +export async function getSessionDataDateSeries( + ...args: [ + websiteId: string, + propertyName: string, + filters: QueryFilters, + propertyFilters?: PropertyFilter[], + ] +): Promise { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery( + websiteId: string, + propertyName: string, + filters: QueryFilters, + propertyFilters: PropertyFilter[] = [], +): Promise { + const { timezone = 'utc' } = filters; + const { rawQuery, parseFilters, getDateStringSQL, getPropertyFilterQuery } = prisma; + const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + timezone, + }); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(propertyFilters, 'session', timezone); + + return rawQuery( + ` + select + ${getDateStringSQL('session_data.date_value', 'second', timezone)} as t, + count(distinct session_data.session_id) as y + from website_event + ${cohortQuery} + ${joinSessionQuery} + join session_data + on session_data.session_id = website_event.session_id + and session_data.website_id = website_event.website_id + where website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + and session_data.data_key = {{propertyName}} + and session_data.data_type = ${DATA_TYPE.date} + ${filterQuery} + ${pfSQL} + group by 1 + order by 1 + `, + { ...queryParams, propertyName, ...pfParams }, + FUNCTION_NAME, + ); +} + +async function clickhouseQuery( + websiteId: string, + propertyName: string, + filters: QueryFilters, + propertyFilters: PropertyFilter[] = [], +): Promise { + const { timezone = 'UTC' } = filters; + const { rawQuery, parseFilters, getDateStringSQL, getPropertyFilterQuery } = clickhouse; + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(propertyFilters, 'session', timezone); + + return rawQuery( + ` + select + ${getDateStringSQL('session_data.date_value', 'second', timezone)} as t, + uniq(session_data.session_id) as y + from website_event + ${cohortQuery} + join session_data final + on session_data.session_id = website_event.session_id + and session_data.website_id = {websiteId:UUID} + where website_event.website_id = {websiteId:UUID} + and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and session_data.data_key = {propertyName:String} + and session_data.data_type = ${DATA_TYPE.date} + ${filterQuery} + ${pfSQL} + group by t + order by t + `, + { ...queryParams, propertyName, ...pfParams }, + FUNCTION_NAME, + ); +} diff --git a/src/queries/sql/sessions/getSessionDataNumericSeries.ts b/src/queries/sql/sessions/getSessionDataNumericSeries.ts new file mode 100644 index 000000000..03d660f02 --- /dev/null +++ b/src/queries/sql/sessions/getSessionDataNumericSeries.ts @@ -0,0 +1,119 @@ +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import type { PropertyFilter, QueryFilters } from '@/lib/types'; + +const FUNCTION_NAME = 'getSessionDataNumericSeries'; + +export async function getSessionDataNumericSeries( + ...args: [ + websiteId: string, + propertyName: string, + metric: 'sum' | 'avg' | 'count', + filters: QueryFilters, + propertyFilters?: PropertyFilter[], + ] +) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery( + websiteId: string, + propertyName: string, + metric: 'sum' | 'avg' | 'count', + filters: QueryFilters, + propertyFilters: PropertyFilter[] = [], +) { + const { timezone = 'utc', unit = 'day' } = filters; + const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = prisma; + const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + timezone, + }); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(propertyFilters, 'session', timezone); + const aggSql = + metric === 'avg' + ? 'avg(cast(session_data.number_value as decimal))' + : metric === 'count' + ? 'count(distinct session_data.session_id)' + : 'sum(cast(session_data.number_value as decimal))'; + + return rawQuery( + ` + with filtered_sessions as ( + select distinct website_event.session_id, website_event.website_id + from website_event + ${cohortQuery} + ${joinSessionQuery} + where website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + ${filterQuery} + ${pfSQL} + ) + select + ${getDateSQL('session_data.created_at', unit, timezone)} t, + ${aggSql} y + from session_data + join filtered_sessions + on filtered_sessions.session_id = session_data.session_id + and filtered_sessions.website_id = session_data.website_id + where session_data.website_id = {{websiteId::uuid}} + and session_data.data_key = {{propertyName}} + and session_data.data_type = 2 + group by 1 + order by 1 + `, + { ...queryParams, propertyName, ...pfParams }, + FUNCTION_NAME, + ); +} + +async function clickhouseQuery( + websiteId: string, + propertyName: string, + metric: 'sum' | 'avg' | 'count', + filters: QueryFilters, + propertyFilters: PropertyFilter[] = [], +): Promise<{ t: string; y: number }[]> { + const { timezone = 'UTC', unit = 'day' } = filters; + const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = clickhouse; + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(propertyFilters, 'session', timezone); + const aggSql = + metric === 'avg' + ? 'avg(session_data.number_value)' + : metric === 'count' + ? 'uniq(session_data.session_id)' + : 'sum(session_data.number_value)'; + + return rawQuery( + ` + with filtered_sessions as ( + select distinct website_event.session_id + from website_event + ${cohortQuery} + where website_event.website_id = {websiteId:UUID} + and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + ${filterQuery} + ${pfSQL} + ) + select + ${getDateSQL('session_data.created_at', unit, timezone)} as t, + ${aggSql} as y + from session_data final + join filtered_sessions + on filtered_sessions.session_id = session_data.session_id + where session_data.website_id = {websiteId:UUID} + and session_data.data_key = {propertyName:String} + and session_data.data_type = 2 + group by t + order by t + `, + { ...queryParams, propertyName, ...pfParams }, + FUNCTION_NAME, + ); +} diff --git a/src/queries/sql/sessions/getSessionDataNumericStats.ts b/src/queries/sql/sessions/getSessionDataNumericStats.ts new file mode 100644 index 000000000..a7dcd7df0 --- /dev/null +++ b/src/queries/sql/sessions/getSessionDataNumericStats.ts @@ -0,0 +1,106 @@ +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import type { EventDataNumericStats, PropertyFilter, QueryFilters } from '@/lib/types'; + +const FUNCTION_NAME = 'getSessionDataNumericStats'; + +export async function getSessionDataNumericStats( + ...args: [ + websiteId: string, + propertyName: string, + filters: QueryFilters, + propertyFilters?: PropertyFilter[], + ] +): Promise { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }).then(results => results?.[0]); +} + +async function relationalQuery( + websiteId: string, + propertyName: string, + filters: QueryFilters, + propertyFilters: PropertyFilter[] = [], +) { + const { timezone = 'utc' } = filters; + const { rawQuery, parseFilters, getPropertyFilterQuery } = prisma; + const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + timezone, + }); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(propertyFilters, 'session', timezone); + + return rawQuery( + ` + with filtered_sessions as ( + select distinct website_event.session_id, website_event.website_id + from website_event + ${cohortQuery} + ${joinSessionQuery} + where website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + ${filterQuery} + ${pfSQL} + ) + select + coalesce(sum(cast(session_data.number_value as decimal)), 0) as "total", + coalesce(avg(cast(session_data.number_value as decimal)), 0) as "average", + coalesce(percentile_cont(0.5) within group (order by session_data.number_value), 0) as "median", + coalesce(max(session_data.number_value), 0) as "max", + coalesce(min(session_data.number_value), 0) as "min" + from session_data + join filtered_sessions + on filtered_sessions.session_id = session_data.session_id + and filtered_sessions.website_id = session_data.website_id + where session_data.website_id = {{websiteId::uuid}} + and session_data.data_key = {{propertyName}} + and session_data.data_type = 2 + `, + { ...queryParams, propertyName, ...pfParams }, + FUNCTION_NAME, + ); +} + +async function clickhouseQuery( + websiteId: string, + propertyName: string, + filters: QueryFilters, + propertyFilters: PropertyFilter[] = [], +): Promise { + const { timezone = 'UTC' } = filters; + const { rawQuery, parseFilters, getPropertyFilterQuery } = clickhouse; + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(propertyFilters, 'session', timezone); + + return rawQuery( + ` + with filtered_sessions as ( + select distinct website_event.session_id + from website_event + ${cohortQuery} + where website_event.website_id = {websiteId:UUID} + and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + ${filterQuery} + ${pfSQL} + ) + select + if(count() = 0, 0, sum(session_data.number_value)) as total, + if(count() = 0, 0, avg(session_data.number_value)) as average, + if(count() = 0, 0, median(session_data.number_value)) as median, + if(count() = 0, 0, max(session_data.number_value)) as max, + if(count() = 0, 0, min(session_data.number_value)) as min + from session_data final + join filtered_sessions + on filtered_sessions.session_id = session_data.session_id + where session_data.website_id = {websiteId:UUID} + and session_data.data_key = {propertyName:String} + and session_data.data_type = 2 + `, + { ...queryParams, propertyName, ...pfParams }, + FUNCTION_NAME, + ); +} diff --git a/src/queries/sql/sessions/getSessionDataPivot.ts b/src/queries/sql/sessions/getSessionDataPivot.ts new file mode 100644 index 000000000..6ea7b6b5f --- /dev/null +++ b/src/queries/sql/sessions/getSessionDataPivot.ts @@ -0,0 +1,215 @@ +import clickhouse from '@/lib/clickhouse'; +import { DEFAULT_PAGE_SIZE } from '@/lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import type { PageResult, PropertyFilter, QueryFilters, SessionDataPivotRow } from '@/lib/types'; + +const FUNCTION_NAME = 'getSessionDataPivot'; + +export async function getSessionDataPivot( + ...args: [ + websiteId: string, + propertyName: string, + filters: QueryFilters, + propertyFilters?: PropertyFilter[], + ] +): Promise> { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery( + websiteId: string, + propertyName: string, + filters: QueryFilters, + propertyFilters: PropertyFilter[] = [], +): Promise> { + const { timezone = 'utc' } = filters; + const { rawQuery, parseFilters, getPropertyFilterQuery } = prisma; + const { page = 1, pageSize } = filters; + const size = +pageSize || DEFAULT_PAGE_SIZE; + const offset = +size * (+page - 1); + + const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + timezone, + }); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery( + propertyFilters, + 'session', + timezone, + ); + + const countResult = await rawQuery( + ` + with filtered_sessions as ( + select distinct website_event.session_id + from website_event + ${cohortQuery} + ${joinSessionQuery} + where website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + ${filterQuery} + ${pfSQL} + ), + selected_property_sessions as ( + select distinct session_data.session_id + from session_data + join filtered_sessions + on filtered_sessions.session_id = session_data.session_id + where session_data.website_id = {{websiteId::uuid}} + and session_data.data_key = {{propertyName}} + ) + select count(*) as num + from selected_property_sessions + `, + { ...queryParams, websiteId, propertyName, ...pfParams }, + ); + + const count = countResult[0].num; + + const rows = await rawQuery( + ` + with filtered_sessions as ( + select distinct website_event.session_id + from website_event + ${cohortQuery} + ${joinSessionQuery} + where website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + ${filterQuery} + ${pfSQL} + ), + selected_property_sessions as ( + select distinct session_data.session_id + from session_data + join filtered_sessions + on filtered_sessions.session_id = session_data.session_id + where session_data.website_id = {{websiteId::uuid}} + and session_data.data_key = {{propertyName}} + ), + paged_sessions as ( + select session_data.session_id + from session_data + join selected_property_sessions + on selected_property_sessions.session_id = session_data.session_id + where session_data.website_id = {{websiteId::uuid}} + group by session_data.session_id + order by max(session_data.created_at) desc + limit ${size} offset ${offset} + ) + select + session_data.session_id as "sessionId", + coalesce(max(session_data.distinct_id), '') as "distinctId", + max(session_data.created_at) as "createdAt", + array_agg(session_data.data_key order by session_data.data_key asc) as "propertyKeys", + array_agg( + coalesce( + case when session_data.data_type = 1 then session_data.string_value end, + case when session_data.data_type = 2 then cast(session_data.number_value as varchar) end, + case when session_data.data_type = 3 then session_data.string_value end, + case when session_data.data_type = 4 then cast(session_data.date_value as varchar) end, + case when session_data.data_type = 5 then session_data.string_value end, + '' + ) + order by session_data.data_key asc + ) as "propertyValues" + from session_data + join paged_sessions on paged_sessions.session_id = session_data.session_id + where session_data.website_id = {{websiteId::uuid}} + group by session_data.session_id + order by max(session_data.created_at) desc + `, + { ...queryParams, websiteId, propertyName, ...pfParams }, + FUNCTION_NAME, + ); + + return { data: rows, count, page: +page, pageSize: size }; +} + +async function clickhouseQuery( + websiteId: string, + propertyName: string, + filters: QueryFilters, + propertyFilters: PropertyFilter[] = [], +): Promise> { + const { timezone = 'UTC' } = filters; + const { rawQuery, parseFilters, getPropertyFilterQuery } = clickhouse; + const { page = 1, pageSize } = filters; + const size = +pageSize || DEFAULT_PAGE_SIZE; + const offset = +size * (+page - 1); + + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery( + propertyFilters, + 'session', + timezone, + ); + + const count = await rawQuery( + ` + with filtered_sessions as ( + select distinct website_event.session_id + from website_event + ${cohortQuery} + where website_event.website_id = {websiteId:UUID} + and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + ${filterQuery} + ${pfSQL} + ), + selected_property_sessions as ( + select distinct session_data.session_id + from session_data final + join filtered_sessions on filtered_sessions.session_id = session_data.session_id + where session_data.website_id = {websiteId:UUID} + and session_data.data_key = {propertyName:String} + ) + select count() as num + from selected_property_sessions + `, + { ...queryParams, websiteId, propertyName, ...pfParams }, + ).then((res: any) => res[0].num); + + const data = await rawQuery( + ` + with filtered_sessions as ( + select distinct website_event.session_id + from website_event + ${cohortQuery} + where website_event.website_id = {websiteId:UUID} + and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + ${filterQuery} + ${pfSQL} + ), + selected_property_sessions as ( + select distinct session_data.session_id + from session_data final + join filtered_sessions on filtered_sessions.session_id = session_data.session_id + where session_data.website_id = {websiteId:UUID} + and session_data.data_key = {propertyName:String} + ) + select + session_data_pivot.session_id as sessionId, + session_data_pivot.distinct_id as distinctId, + maxMerge(session_data_pivot.created_at) as createdAt, + groupArrayMerge(session_data_pivot.property_keys) as propertyKeys, + groupArrayMerge(session_data_pivot.property_values) as propertyValues + from umami.session_data_pivot session_data_pivot + join selected_property_sessions + on selected_property_sessions.session_id = session_data_pivot.session_id + where session_data_pivot.website_id = {websiteId:UUID} + group by + session_data_pivot.session_id, + session_data_pivot.distinct_id + order by createdAt desc + limit ${size} offset ${offset} + `, + { ...queryParams, websiteId, propertyName, ...pfParams }, + FUNCTION_NAME, + ); + + return { data, count, page: +page, pageSize: size }; +} diff --git a/src/queries/sql/sessions/getSessionDataProperties.ts b/src/queries/sql/sessions/getSessionDataProperties.ts index 9b429f9b9..446d8e465 100644 --- a/src/queries/sql/sessions/getSessionDataProperties.ts +++ b/src/queries/sql/sessions/getSessionDataProperties.ts @@ -1,12 +1,17 @@ import clickhouse from '@/lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; -import type { QueryFilters } from '@/lib/types'; +import type { PropertyFilter, QueryFilters } from '@/lib/types'; const FUNCTION_NAME = 'getSessionDataProperties'; export async function getSessionDataProperties( - ...args: [websiteId: string, filters: QueryFilters] + ...args: [ + websiteId: string, + filters: QueryFilters, + propertyFilters?: PropertyFilter[], + selectedPropertyName?: string, + ] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -14,32 +19,58 @@ export async function getSessionDataProperties( }); } -async function relationalQuery(websiteId: string, filters: QueryFilters) { - const { rawQuery, parseFilters } = prisma; +async function relationalQuery( + websiteId: string, + filters: QueryFilters, + propertyFilters: PropertyFilter[] = [], + selectedPropertyName?: string, +) { + const { timezone = 'utc' } = filters; + const { rawQuery, parseFilters, getPropertyFilterQuery } = prisma; const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, + timezone, }); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery( + propertyFilters, + 'session', + timezone, + ); return rawQuery( ` + with filtered_sessions as ( + select distinct website_event.session_id, website_event.website_id + from website_event + ${cohortQuery} + ${joinSessionQuery} + where website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + ${filterQuery} + ${pfSQL} + ), + selected_property_sessions as ( + select distinct session_data.session_id, session_data.website_id + from session_data + join filtered_sessions + on filtered_sessions.session_id = session_data.session_id + and filtered_sessions.website_id = session_data.website_id + ${selectedPropertyName ? 'where session_data.data_key = {{selectedPropertyName}}' : ''} + ) select data_key as "propertyName", + data_type as "dataType", count(distinct session_data.session_id) as "total" - from website_event - ${cohortQuery} - ${joinSessionQuery} + from selected_property_sessions join session_data - on session_data.session_id = website_event.session_id - and session_data.website_id = website_event.website_id - where website_event.website_id = {{websiteId::uuid}} - and website_event.created_at between {{startDate}} and {{endDate}} - ${filterQuery} - group by 1 - order by 2 desc + on session_data.session_id = selected_property_sessions.session_id + and session_data.website_id = selected_property_sessions.website_id + group by 1, 2 + order by 3 desc, 1 asc limit 500 `, - queryParams, + { ...queryParams, websiteId, selectedPropertyName, ...pfParams }, FUNCTION_NAME, ); } @@ -47,29 +78,52 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { async function clickhouseQuery( websiteId: string, filters: QueryFilters, -): Promise<{ propertyName: string; total: number }[]> { - const { rawQuery, parseFilters } = clickhouse; - const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId }); + propertyFilters: PropertyFilter[] = [], + selectedPropertyName?: string, +): Promise<{ propertyName: string; dataType: number; total: number }[]> { + const { timezone = 'UTC' } = filters; + const { rawQuery, parseFilters, getPropertyFilterQuery } = clickhouse; + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery( + propertyFilters, + 'session', + timezone, + ); return rawQuery( ` + with filtered_sessions as ( + select distinct website_event.session_id, website_event.website_id + from website_event + ${cohortQuery} + where website_event.website_id = {websiteId:UUID} + and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + ${filterQuery} + ${pfSQL} + ), + selected_property_sessions as ( + select distinct session_data.session_id, session_data.website_id + from session_data final + join filtered_sessions + on filtered_sessions.session_id = session_data.session_id + and filtered_sessions.website_id = session_data.website_id + ${selectedPropertyName ? 'where session_data.data_key = {selectedPropertyName:String}' : ''} + ) select data_key as propertyName, + data_type as dataType, count(distinct session_data.session_id) as total - from website_event - ${cohortQuery} + from selected_property_sessions join session_data final - on session_data.session_id = website_event.session_id - and session_data.website_id = {websiteId:UUID} - where website_event.website_id = {websiteId:UUID} - and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + on session_data.session_id = selected_property_sessions.session_id + and session_data.website_id = selected_property_sessions.website_id + where session_data.website_id = {websiteId:UUID} and session_data.data_key != '' - ${filterQuery} - group by 1 - order by 2 desc + group by 1, 2 + order by 3 desc, 1 asc limit 500 `, - queryParams, + { ...queryParams, websiteId, selectedPropertyName, ...pfParams }, FUNCTION_NAME, ); } diff --git a/src/queries/sql/sessions/getSessionDataPropertySeries.ts b/src/queries/sql/sessions/getSessionDataPropertySeries.ts new file mode 100644 index 000000000..fdffab423 --- /dev/null +++ b/src/queries/sql/sessions/getSessionDataPropertySeries.ts @@ -0,0 +1,98 @@ +import clickhouse from '@/lib/clickhouse'; +import { DATA_TYPE } from '@/lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import type { EventDataSeriesPoint, PropertyFilter, QueryFilters } from '@/lib/types'; + +const FUNCTION_NAME = 'getSessionDataPropertySeries'; + +export async function getSessionDataPropertySeries( + ...args: [ + websiteId: string, + propertyName: string, + filters: QueryFilters, + propertyFilters?: PropertyFilter[], + ] +): Promise { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery( + websiteId: string, + propertyName: string, + filters: QueryFilters, + propertyFilters: PropertyFilter[] = [], +): Promise { + const { timezone = 'utc', unit = 'day' } = filters; + const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = prisma; + const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + timezone, + }); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(propertyFilters, 'session', timezone); + + return rawQuery( + ` + select + session_data.string_value as x, + ${getDateSQL('session_data.created_at', unit, timezone)} t, + count(distinct session_data.session_id) y + from website_event + ${cohortQuery} + ${joinSessionQuery} + join session_data + on session_data.session_id = website_event.session_id + and session_data.website_id = website_event.website_id + where website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + and session_data.data_key = {{propertyName}} + and session_data.data_type in (${DATA_TYPE.string}, ${DATA_TYPE.boolean}) + ${filterQuery} + ${pfSQL} + group by 1, 2 + order by 2 + `, + { ...queryParams, propertyName, ...pfParams }, + FUNCTION_NAME, + ); +} + +async function clickhouseQuery( + websiteId: string, + propertyName: string, + filters: QueryFilters, + propertyFilters: PropertyFilter[] = [], +): Promise { + const { timezone = 'UTC', unit = 'day' } = filters; + const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = clickhouse; + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(propertyFilters, 'session', timezone); + + return rawQuery( + ` + select + session_data.string_value as x, + ${getDateSQL('session_data.created_at', unit, timezone)} as t, + uniq(session_data.session_id) as y + from website_event + ${cohortQuery} + join session_data final + on session_data.session_id = website_event.session_id + and session_data.website_id = {websiteId:UUID} + where website_event.website_id = {websiteId:UUID} + and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and session_data.data_key = {propertyName:String} + and session_data.data_type in (${DATA_TYPE.string}, ${DATA_TYPE.boolean}) + ${filterQuery} + ${pfSQL} + group by x, t + order by t + `, + { ...queryParams, propertyName, ...pfParams }, + FUNCTION_NAME, + ); +} diff --git a/src/queries/sql/sessions/getSessionDataStats.ts b/src/queries/sql/sessions/getSessionDataStats.ts new file mode 100644 index 000000000..d0ea361cd --- /dev/null +++ b/src/queries/sql/sessions/getSessionDataStats.ts @@ -0,0 +1,185 @@ +import clickhouse from '@/lib/clickhouse'; +import { DATA_TYPE } from '@/lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import type { PropertyFilter, PropertyLeaderboardRow, QueryFilters } from '@/lib/types'; + +const FUNCTION_NAME = 'getSessionDataStats'; + +export async function getSessionDataStats( + ...args: [websiteId: string, propertyName: string, filters: QueryFilters, propertyFilters?: PropertyFilter[]] +): Promise { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery( + websiteId: string, + propertyName: string, + filters: QueryFilters, + propertyFilters: PropertyFilter[] = [], +) { + const { timezone = 'utc' } = filters; + const { rawQuery, parseFilters, getPropertyFilterQuery, getTimestampDiffSQL } = prisma; + const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + timezone, + }); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(propertyFilters, 'session', timezone); + + return rawQuery( + ` + with filtered_sessions as ( + select distinct website_event.session_id, website_event.website_id + from website_event + ${cohortQuery} + ${joinSessionQuery} + where website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + ${filterQuery} + ${pfSQL} + ), + session_rollup as ( + select + website_event.session_id, + website_event.visit_id, + count(*) as activity, + sum(case when website_event.event_type = 1 then 1 else 0 end) as views, + sum(case when website_event.event_type = 2 then 1 else 0 end) as events, + min(website_event.created_at) as min_time, + max(website_event.created_at) as max_time + from website_event + join filtered_sessions + on filtered_sessions.session_id = website_event.session_id + and filtered_sessions.website_id = website_event.website_id + where website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + group by website_event.session_id, website_event.visit_id + ), + session_stats as ( + select + session_id, + count(*) as visits, + sum(activity) as activity, + sum(views) as views, + sum(events) as events, + sum(${getTimestampDiffSQL('min_time', 'max_time')}) as totaltime + from session_rollup + group by session_id + ), + property_values as ( + select + session_data.session_id, + session_data.string_value as value + from session_data + join filtered_sessions + on filtered_sessions.session_id = session_data.session_id + and filtered_sessions.website_id = session_data.website_id + where session_data.website_id = {{websiteId::uuid}} + and session_data.data_key = {{propertyName}} + and session_data.data_type = ${DATA_TYPE.string} + and coalesce(session_data.string_value, '') != '' + ) + select + property_values.value as label, + coalesce(sum(session_stats.activity), 0) as activity, + count(distinct property_values.session_id) as sessions, + coalesce(sum(session_stats.visits), 0) as visits, + coalesce(sum(session_stats.views), 0) as views, + coalesce(sum(session_stats.events), 0) as events, + coalesce(sum(session_stats.totaltime), 0) as totaltime, + coalesce(sum(case when session_stats.visits = 1 then 1 else 0 end), 0) as "newSessions", + coalesce(sum(case when session_stats.visits > 1 then 1 else 0 end), 0) as "returningSessions" + from property_values + join session_stats on session_stats.session_id = property_values.session_id + group by property_values.value + order by activity desc, property_values.value asc + limit 100 + `, + { ...queryParams, websiteId, propertyName, ...pfParams }, + FUNCTION_NAME, + ); +} + +async function clickhouseQuery( + websiteId: string, + propertyName: string, + filters: QueryFilters, + propertyFilters: PropertyFilter[] = [], +) { + const { timezone = 'UTC' } = filters; + const { rawQuery, parseFilters, getPropertyFilterQuery } = clickhouse; + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(propertyFilters, 'session', timezone); + + return rawQuery( + ` + with filtered_sessions as ( + select distinct website_event.session_id + from website_event + ${cohortQuery} + where website_event.website_id = {websiteId:UUID} + and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + ${filterQuery} + ${pfSQL} + ), + session_rollup as ( + select + website_event.session_id, + website_event.visit_id, + count() as activity, + sumIf(1, website_event.event_type = 1) as views, + sumIf(1, website_event.event_type = 2) as events, + min(website_event.created_at) as min_time, + max(website_event.created_at) as max_time + from website_event + join filtered_sessions on filtered_sessions.session_id = website_event.session_id + where website_event.website_id = {websiteId:UUID} + and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + group by website_event.session_id, website_event.visit_id + ), + session_stats as ( + select + session_id, + count() as visits, + sum(activity) as activity, + sum(views) as views, + sum(events) as events, + sum(max_time - min_time) as totaltime + from session_rollup + group by session_id + ), + property_values as ( + select + session_data.session_id, + session_data.string_value as value + from session_data final + join filtered_sessions on filtered_sessions.session_id = session_data.session_id + where session_data.website_id = {websiteId:UUID} + and session_data.data_key = {propertyName:String} + and session_data.data_type = ${DATA_TYPE.string} + and ifNull(session_data.string_value, '') != '' + ) + select + property_values.value as label, + ifNull(sum(session_stats.activity), 0) as activity, + uniq(property_values.session_id) as sessions, + ifNull(sum(session_stats.visits), 0) as visits, + ifNull(sum(session_stats.views), 0) as views, + ifNull(sum(session_stats.events), 0) as events, + ifNull(sum(session_stats.totaltime), 0) as totaltime, + ifNull(sum(if(session_stats.visits = 1, 1, 0)), 0) as newSessions, + ifNull(sum(if(session_stats.visits > 1, 1, 0)), 0) as returningSessions + from property_values + join session_stats on session_stats.session_id = property_values.session_id + group by property_values.value + order by activity desc, property_values.value asc + limit 100 + `, + { ...queryParams, websiteId, propertyName, ...pfParams }, + FUNCTION_NAME, + ); +} diff --git a/src/queries/sql/sessions/getSessionDataValues.ts b/src/queries/sql/sessions/getSessionDataValues.ts index 5790141c5..e015e30ef 100644 --- a/src/queries/sql/sessions/getSessionDataValues.ts +++ b/src/queries/sql/sessions/getSessionDataValues.ts @@ -1,4 +1,5 @@ import clickhouse from '@/lib/clickhouse'; +import { DATA_TYPE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { QueryFilters } from '@/lib/types'; @@ -6,7 +7,7 @@ import type { QueryFilters } from '@/lib/types'; const FUNCTION_NAME = 'getSessionDataValues'; export async function getSessionDataValues( - ...args: [websiteId: string, filters: QueryFilters & { propertyName?: string }] + ...args: [websiteId: string, filters: QueryFilters & { propertyName?: string; dataType?: number }] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -16,14 +17,42 @@ export async function getSessionDataValues( async function relationalQuery( websiteId: string, - filters: QueryFilters & { propertyName?: string }, + filters: QueryFilters & { propertyName?: string; dataType?: number }, ) { const { rawQuery, parseFilters, getDateSQL } = prisma; + const { dataType } = filters; const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, }); + if (dataType === DATA_TYPE.array) { + return rawQuery( + ` + select + array_item.value as "value", + count(distinct session_data.session_id) as "total" + from website_event + ${cohortQuery} + ${joinSessionQuery} + join session_data + on session_data.session_id = website_event.session_id + and session_data.website_id = website_event.website_id + cross join lateral jsonb_array_elements_text(coalesce(session_data.string_value, '[]')::jsonb) as array_item(value) + where website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + and session_data.data_key = {{propertyName}} + and session_data.data_type = ${DATA_TYPE.array} + ${filterQuery} + group by array_item.value + order by 2 desc + limit 100 + `, + queryParams, + FUNCTION_NAME, + ); + } + return rawQuery( ` select @@ -42,6 +71,7 @@ async function relationalQuery( where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} and session_data.data_key = {{propertyName}} + ${dataType ? `and session_data.data_type = ${dataType}` : ''} ${filterQuery} group by value order by 2 desc @@ -54,11 +84,37 @@ async function relationalQuery( async function clickhouseQuery( websiteId: string, - filters: QueryFilters & { propertyName?: string }, + filters: QueryFilters & { propertyName?: string; dataType?: number }, ): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> { const { rawQuery, parseFilters } = clickhouse; + const { dataType } = filters; const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId }); + if (dataType === DATA_TYPE.array) { + return rawQuery( + ` + select + arrayJoin(JSONExtract(ifNull(session_data.string_value, '[]'), 'Array(String)')) as "value", + uniq(session_data.session_id) as "total" + from website_event + ${cohortQuery} + join session_data final + on session_data.session_id = website_event.session_id + and session_data.website_id = {websiteId:UUID} + where website_event.website_id = {websiteId:UUID} + and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and session_data.data_key = {propertyName:String} + and session_data.data_type = ${DATA_TYPE.array} + ${filterQuery} + group by value + order by 2 desc + limit 100 + `, + queryParams, + FUNCTION_NAME, + ); + } + return rawQuery( ` select @@ -74,6 +130,7 @@ async function clickhouseQuery( where website_event.website_id = {websiteId:UUID} and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} and session_data.data_key = {propertyName:String} + ${dataType ? `and session_data.data_type = ${dataType}` : ''} ${filterQuery} group by value order by 2 desc From 790354d3ac7baf6a3d6bb211399ff6ae016a7ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20F=C3=A9=20Santos?= Date: Thu, 30 Apr 2026 10:48:16 +0100 Subject: [PATCH 53/59] Add data-auto-pageview attribute to tracker script Allow disabling the automatic initial pageview while keeping performance tracking, click handling, and path change detection active. --- src/tracker/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tracker/index.js b/src/tracker/index.js index 85ad2ffea..68b7fe77b 100644 --- a/src/tracker/index.js +++ b/src/tracker/index.js @@ -37,6 +37,7 @@ const domain = config('domains') || ''; const credentials = config('fetch-credentials') || 'omit'; const perf = config('performance') === _true; + const autoPageview = config('auto-pageview') !== _false; const domains = domain.split(',').map(n => n.trim()); const host = @@ -197,7 +198,7 @@ const init = () => { if (!initialized) { initialized = true; - track(); + if (autoPageview) track(); handlePathChanges(); handleClicks(); if (perf) initPerformance(); From 8ce833c93be08367f16d51b8e1aa6f73b51c0f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20F=C3=A9=20Santos?= Date: Thu, 30 Apr 2026 11:22:30 +0100 Subject: [PATCH 54/59] fix: suppress SPA pageview tracking when auto-pageview is disabled The handlePush function was still calling track() on SPA navigations regardless of the auto-pageview setting. Gate that call behind the autoPageview flag while keeping URL state and performance flush active. --- src/tracker/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tracker/index.js b/src/tracker/index.js index 68b7fe77b..521a1f717 100644 --- a/src/tracker/index.js +++ b/src/tracker/index.js @@ -91,7 +91,7 @@ currentRef = currentUrl; currentUrl = normalize(new URL(url, location.href).toString()); - if (currentUrl !== currentRef) { + if (currentUrl !== currentRef && autoPageview) { setTimeout(track, delayDuration); } }; From 1f2bd7cad9cace4dd32d28ba0d6b699b02f5ab04 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 1 May 2026 10:00:30 -0700 Subject: [PATCH 55/59] fix property filter css. format date columns in pivot table --- .../[websiteId]/event-data-pivot/route.ts | 4 +- .../[websiteId]/session-data-pivot/route.ts | 4 +- .../property-data/PropertyFilterEditForm.tsx | 2 +- src/queries/sql/events/getEventDataPivot.ts | 16 +- .../sql/sessions/getSessionDataPivot.ts | 146 ++++++++++++------ 5 files changed, 121 insertions(+), 51 deletions(-) diff --git a/src/app/api/websites/[websiteId]/event-data-pivot/route.ts b/src/app/api/websites/[websiteId]/event-data-pivot/route.ts index c43aa9f3f..048f95a6f 100644 --- a/src/app/api/websites/[websiteId]/event-data-pivot/route.ts +++ b/src/app/api/websites/[websiteId]/event-data-pivot/route.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { parseEventPropertyFilters } from '@/lib/params'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { filterParams, pagingParams } from '@/lib/schema'; +import { filterParams, pagingParams, timezoneParam, unitParam } from '@/lib/schema'; import { canViewWebsite } from '@/permissions'; import { getEventDataPivot } from '@/queries/sql/events/getEventDataPivot'; @@ -14,6 +14,8 @@ export async function GET( startAt: z.coerce.number().int(), endAt: z.coerce.number().int(), eventName: z.string(), + timezone: timezoneParam.optional(), + unit: unitParam.optional(), ...filterParams, ...pagingParams, }); diff --git a/src/app/api/websites/[websiteId]/session-data-pivot/route.ts b/src/app/api/websites/[websiteId]/session-data-pivot/route.ts index f27b17781..6bf0e2b12 100644 --- a/src/app/api/websites/[websiteId]/session-data-pivot/route.ts +++ b/src/app/api/websites/[websiteId]/session-data-pivot/route.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { parsePropertyFilters } from '@/lib/params'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { filterParams, pagingParams } from '@/lib/schema'; +import { filterParams, pagingParams, timezoneParam, unitParam } from '@/lib/schema'; import { canViewWebsite } from '@/permissions'; import { getSessionDataPivot } from '@/queries/sql/sessions/getSessionDataPivot'; @@ -14,6 +14,8 @@ export async function GET( startAt: z.coerce.number().int(), endAt: z.coerce.number().int(), propertyName: z.string(), + timezone: timezoneParam.optional(), + unit: unitParam.optional(), ...filterParams, ...pagingParams, }); diff --git a/src/components/property-data/PropertyFilterEditForm.tsx b/src/components/property-data/PropertyFilterEditForm.tsx index 6d2db0b5f..6f5e9bce6 100644 --- a/src/components/property-data/PropertyFilterEditForm.tsx +++ b/src/components/property-data/PropertyFilterEditForm.tsx @@ -60,7 +60,7 @@ export function PropertyFilterEditForm({ }; return ( - + diff --git a/src/queries/sql/events/getEventDataPivot.ts b/src/queries/sql/events/getEventDataPivot.ts index 7a494a1da..6f7551aed 100644 --- a/src/queries/sql/events/getEventDataPivot.ts +++ b/src/queries/sql/events/getEventDataPivot.ts @@ -22,7 +22,7 @@ export async function getEventDataPivot( async function relationalQuery(websiteId: string, eventName: string, filters: QueryFilters, eventFilters: EventPropertyFilter[] = []) { const { timezone = 'utc' } = filters; - const { rawQuery, parseFilters, getPropertyFilterQuery } = prisma; + const { rawQuery, parseFilters, getPropertyFilterQuery, getDateStringSQL } = prisma; const { page = 1, pageSize } = filters; const size = +pageSize || DEFAULT_PAGE_SIZE; const offset = +size * (+page - 1); @@ -84,7 +84,7 @@ async function relationalQuery(websiteId: string, eventName: string, filters: Qu case when event_data.data_type = 1 then event_data.string_value end, case when event_data.data_type = 2 then cast(event_data.number_value as varchar) end, case when event_data.data_type = 3 then event_data.string_value end, - case when event_data.data_type = 4 then cast(event_data.date_value as varchar) end, + case when event_data.data_type = 4 then ${getDateStringSQL('event_data.date_value', 'second', timezone)} end, case when event_data.data_type = 5 then event_data.string_value end, '' ) as "value", @@ -120,7 +120,7 @@ async function relationalQuery(websiteId: string, eventName: string, filters: Qu async function clickhouseQuery(websiteId: string, eventName: string, filters: QueryFilters, eventFilters: EventPropertyFilter[] = []) { const { timezone = 'UTC' } = filters; - const { rawQuery, parseFilters, getPropertyFilterQuery } = clickhouse; + const { rawQuery, parseFilters, getPropertyFilterQuery, getDateStringSQL } = clickhouse; const { page = 1, pageSize } = filters; const size = +pageSize || DEFAULT_PAGE_SIZE; const offset = +size * (+page - 1); @@ -160,7 +160,15 @@ async function clickhouseQuery(websiteId: string, eventName: string, filters: Qu event_data_pivot.url_path as urlPath, event_data_pivot.created_at as createdAt, groupArrayMerge(property_keys) as propertyKeys, - groupArrayMerge(property_values) as propertyValues + arrayMap( + (value, dataType) -> if( + dataType = 4, + ${getDateStringSQL('parseDateTimeBestEffortOrNull(value)', 'second', timezone)}, + value + ), + groupArrayMerge(property_values), + groupArrayMerge(property_types) + ) as propertyValues from umami.event_data_pivot any left join ( select * diff --git a/src/queries/sql/sessions/getSessionDataPivot.ts b/src/queries/sql/sessions/getSessionDataPivot.ts index 6ea7b6b5f..5ce922008 100644 --- a/src/queries/sql/sessions/getSessionDataPivot.ts +++ b/src/queries/sql/sessions/getSessionDataPivot.ts @@ -27,7 +27,7 @@ async function relationalQuery( propertyFilters: PropertyFilter[] = [], ): Promise> { const { timezone = 'utc' } = filters; - const { rawQuery, parseFilters, getPropertyFilterQuery } = prisma; + const { rawQuery, parseFilters, getPropertyFilterQuery, getDateStringSQL } = prisma; const { page = 1, pageSize } = filters; const size = +pageSize || DEFAULT_PAGE_SIZE; const offset = +size * (+page - 1); @@ -43,7 +43,7 @@ async function relationalQuery( timezone, ); - const countResult = await rawQuery( + const countResult = (await rawQuery( ` with filtered_sessions as ( select distinct website_event.session_id @@ -67,11 +67,11 @@ async function relationalQuery( from selected_property_sessions `, { ...queryParams, websiteId, propertyName, ...pfParams }, - ); + )) as { num: number }[]; const count = countResult[0].num; - const rows = await rawQuery( + const rows = (await rawQuery( ` with filtered_sessions as ( select distinct website_event.session_id @@ -91,41 +91,68 @@ async function relationalQuery( where session_data.website_id = {{websiteId::uuid}} and session_data.data_key = {{propertyName}} ), + latest_session_properties as ( + select + ranked.session_id, + ranked.distinct_id, + ranked.created_at, + ranked.data_key, + ranked.data_type, + ranked.string_value, + ranked.number_value, + ranked.date_value + from ( + select + session_data.session_id, + session_data.distinct_id, + session_data.created_at, + session_data.data_key, + session_data.data_type, + session_data.string_value, + session_data.number_value, + session_data.date_value, + row_number() over ( + partition by session_data.session_id, session_data.data_key + order by session_data.created_at desc, session_data.session_data_id desc + ) as row_num + from session_data + join selected_property_sessions + on selected_property_sessions.session_id = session_data.session_id + where session_data.website_id = {{websiteId::uuid}} + ) ranked + where ranked.row_num = 1 + ), paged_sessions as ( - select session_data.session_id - from session_data - join selected_property_sessions - on selected_property_sessions.session_id = session_data.session_id - where session_data.website_id = {{websiteId::uuid}} - group by session_data.session_id - order by max(session_data.created_at) desc + select latest_session_properties.session_id + from latest_session_properties + group by latest_session_properties.session_id + order by max(latest_session_properties.created_at) desc limit ${size} offset ${offset} ) select - session_data.session_id as "sessionId", - coalesce(max(session_data.distinct_id), '') as "distinctId", - max(session_data.created_at) as "createdAt", - array_agg(session_data.data_key order by session_data.data_key asc) as "propertyKeys", + latest_session_properties.session_id as "sessionId", + coalesce(max(latest_session_properties.distinct_id), '') as "distinctId", + max(latest_session_properties.created_at) as "createdAt", + array_agg(latest_session_properties.data_key order by latest_session_properties.data_key asc) as "propertyKeys", array_agg( coalesce( - case when session_data.data_type = 1 then session_data.string_value end, - case when session_data.data_type = 2 then cast(session_data.number_value as varchar) end, - case when session_data.data_type = 3 then session_data.string_value end, - case when session_data.data_type = 4 then cast(session_data.date_value as varchar) end, - case when session_data.data_type = 5 then session_data.string_value end, + case when latest_session_properties.data_type = 1 then latest_session_properties.string_value end, + case when latest_session_properties.data_type = 2 then cast(latest_session_properties.number_value as varchar) end, + case when latest_session_properties.data_type = 3 then latest_session_properties.string_value end, + case when latest_session_properties.data_type = 4 then ${getDateStringSQL('latest_session_properties.date_value', 'second', timezone)} end, + case when latest_session_properties.data_type = 5 then latest_session_properties.string_value end, '' ) - order by session_data.data_key asc + order by latest_session_properties.data_key asc ) as "propertyValues" - from session_data - join paged_sessions on paged_sessions.session_id = session_data.session_id - where session_data.website_id = {{websiteId::uuid}} - group by session_data.session_id - order by max(session_data.created_at) desc + from latest_session_properties + join paged_sessions on paged_sessions.session_id = latest_session_properties.session_id + group by latest_session_properties.session_id + order by max(latest_session_properties.created_at) desc `, { ...queryParams, websiteId, propertyName, ...pfParams }, FUNCTION_NAME, - ); + )) as SessionDataPivotRow[]; return { data: rows, count, page: +page, pageSize: size }; } @@ -137,7 +164,7 @@ async function clickhouseQuery( propertyFilters: PropertyFilter[] = [], ): Promise> { const { timezone = 'UTC' } = filters; - const { rawQuery, parseFilters, getPropertyFilterQuery } = clickhouse; + const { rawQuery, parseFilters, getPropertyFilterQuery, getDateStringSQL } = clickhouse; const { page = 1, pageSize } = filters; const size = +pageSize || DEFAULT_PAGE_SIZE; const offset = +size * (+page - 1); @@ -149,7 +176,7 @@ async function clickhouseQuery( timezone, ); - const count = await rawQuery( + const countResult = (await rawQuery( ` with filtered_sessions as ( select distinct website_event.session_id @@ -171,9 +198,10 @@ async function clickhouseQuery( from selected_property_sessions `, { ...queryParams, websiteId, propertyName, ...pfParams }, - ).then((res: any) => res[0].num); + )) as { num: number }[]; + const count = countResult[0].num; - const data = await rawQuery( + const data = (await rawQuery( ` with filtered_sessions as ( select distinct website_event.session_id @@ -190,26 +218,56 @@ async function clickhouseQuery( join filtered_sessions on filtered_sessions.session_id = session_data.session_id where session_data.website_id = {websiteId:UUID} and session_data.data_key = {propertyName:String} + ), + latest_session_properties as ( + select + session_data.session_id as session_id, + argMax(ifNull(session_data.distinct_id, ''), session_data.created_at) as distinct_id, + session_data.data_key as data_key, + argMax(session_data.data_type, session_data.created_at) as data_type, + argMax(session_data.string_value, session_data.created_at) as string_value, + argMax(session_data.number_value, session_data.created_at) as number_value, + argMax(session_data.date_value, session_data.created_at) as date_value, + max(session_data.created_at) as created_at + from session_data final + join selected_property_sessions + on selected_property_sessions.session_id = session_data.session_id + where session_data.website_id = {websiteId:UUID} + group by + session_data.session_id, + session_data.data_key + ), + paged_sessions as ( + select + latest_session_properties.session_id + from latest_session_properties + group by latest_session_properties.session_id + order by max(latest_session_properties.created_at) desc + limit ${size} offset ${offset} ) select - session_data_pivot.session_id as sessionId, - session_data_pivot.distinct_id as distinctId, - maxMerge(session_data_pivot.created_at) as createdAt, - groupArrayMerge(session_data_pivot.property_keys) as propertyKeys, - groupArrayMerge(session_data_pivot.property_values) as propertyValues - from umami.session_data_pivot session_data_pivot - join selected_property_sessions - on selected_property_sessions.session_id = session_data_pivot.session_id - where session_data_pivot.website_id = {websiteId:UUID} + latest_session_properties.session_id as sessionId, + max(latest_session_properties.distinct_id) as distinctId, + max(latest_session_properties.created_at) as createdAt, + groupArray(latest_session_properties.data_key) as propertyKeys, + groupArray( + multiIf( + latest_session_properties.data_type IN (1, 3, 5), ifNull(latest_session_properties.string_value, ''), + latest_session_properties.data_type = 2, toString(ifNull(latest_session_properties.number_value, 0)), + latest_session_properties.data_type = 4, ${getDateStringSQL('latest_session_properties.date_value', 'second', timezone)}, + '' + ) + ) as propertyValues + from latest_session_properties + join paged_sessions + on paged_sessions.session_id = latest_session_properties.session_id group by - session_data_pivot.session_id, - session_data_pivot.distinct_id + latest_session_properties.session_id order by createdAt desc - limit ${size} offset ${offset} `, { ...queryParams, websiteId, propertyName, ...pfParams }, FUNCTION_NAME, - ); + )) as SessionDataPivotRow[]; return { data, count, page: +page, pageSize: size }; } From d5c48ee75f91147b3f33c0a7068ae371303058dc Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 1 May 2026 12:09:22 -0700 Subject: [PATCH 56/59] clean-up session property string charts and insights. --- public/intl/messages/en-US.json | 1 + .../HighCardinalityPropertyChart.tsx | 133 -------------- .../session-data/SessionPropertyChart.tsx | 136 ++++++++++++++ .../SessionStringPropertyView.tsx | 53 ------ .../sessions/SessionProperties.tsx | 4 +- .../[websiteId]/session-data/stats/route.ts | 4 +- .../charts/DistributionBarChart.tsx | 172 ++++++++++++++++++ src/components/hooks/index.ts | 2 +- ...ts => useSessionDataActivityStatsQuery.ts} | 4 +- src/components/messages.ts | 1 + src/lib/types.ts | 7 - src/queries/sql/index.ts | 2 +- ...tats.ts => getSessionDataActivityStats.ts} | 12 +- .../sql/sessions/getSessionDataPivot.ts | 94 +++++----- .../sql/sessions/getSessionDataValues.ts | 16 +- 15 files changed, 381 insertions(+), 260 deletions(-) delete mode 100644 src/app/(main)/websites/[websiteId]/session-data/HighCardinalityPropertyChart.tsx create mode 100644 src/app/(main)/websites/[websiteId]/session-data/SessionPropertyChart.tsx delete mode 100644 src/app/(main)/websites/[websiteId]/session-data/SessionStringPropertyView.tsx create mode 100644 src/components/charts/DistributionBarChart.tsx rename src/components/hooks/queries/{useSessionDataStatsQuery.ts => useSessionDataActivityStatsQuery.ts} (91%) rename src/queries/sql/sessions/{getSessionDataStats.ts => getSessionDataActivityStats.ts} (92%) diff --git a/public/intl/messages/en-US.json b/public/intl/messages/en-US.json index 38a7460d3..6614c6500 100644 --- a/public/intl/messages/en-US.json +++ b/public/intl/messages/en-US.json @@ -126,6 +126,7 @@ "filter-raw": "Raw", "filters": "Filters", "property-filter": "Property Filter", + "activity-by-property": "Activity by property", "first-click": "First click", "first-seen": "First seen", "funnel": "Funnel", diff --git a/src/app/(main)/websites/[websiteId]/session-data/HighCardinalityPropertyChart.tsx b/src/app/(main)/websites/[websiteId]/session-data/HighCardinalityPropertyChart.tsx deleted file mode 100644 index 1dd39d757..000000000 --- a/src/app/(main)/websites/[websiteId]/session-data/HighCardinalityPropertyChart.tsx +++ /dev/null @@ -1,133 +0,0 @@ -'use client'; -import { PieChart } from '@/components/charts/PieChart'; -import { Empty } from '@/components/common/Empty'; -import { useMessages, useMobile } from '@/components/hooks'; -import { ListTable } from '@/components/metrics/ListTable'; -import { CHART_COLORS } from '@/lib/constants'; -import { formatLongNumber, formatShortTime } from '@/lib/format'; -import type { PropertyLeaderboardRow } from '@/lib/types'; -import { Column, DataColumn, DataTable, Grid, Row, Text } from '@umami/react-zen'; -import { useMemo } from 'react'; - -export function HighCardinalityPropertyChart({ rows }: { rows: PropertyLeaderboardRow[] }) { - const { t, labels } = useMessages(); - const { isPhone } = useMobile(); - const activeTable = useMemo(() => rows.filter(row => row.activity > 0).slice(0, 10), [rows]); - const userMix = useMemo(() => { - const totalNew = rows.reduce((sum, row) => sum + row.newSessions, 0); - const totalReturning = rows.reduce((sum, row) => sum + row.returningSessions, 0); - const total = totalNew + totalReturning; - - return { - newCount: totalNew, - returningCount: totalReturning, - newRate: total ? (totalNew / total) * 100 : 0, - returningRate: total ? (totalReturning / total) * 100 : 0, - }; - }, [rows]); - const sessionTypeTable = useMemo(() => { - return [ - { - label: 'New', - count: userMix.newCount, - percent: userMix.newRate, - }, - { - label: 'Returning', - count: userMix.returningCount, - percent: userMix.returningRate, - }, - ].filter(row => row.count > 0); - }, [userMix]); - const sessionTypeChartData = useMemo(() => { - if (!sessionTypeTable.length) return null; - - return { - labels: sessionTypeTable.map(row => row.label), - datasets: [ - { - data: sessionTypeTable.map(row => row.count), - backgroundColor: [CHART_COLORS[0], CHART_COLORS[1]], - borderWidth: 0, - }, - ], - }; - }, [sessionTypeTable]); - - return ( - - - - - {activeTable.length === 0 ? ( - - ) : isPhone ? ( - - - - {row => ( - - - {row.label} - - - )} - - - - ) : ( - - - - - {row => ( - - - {row.label} - - - )} - - - {row => formatLongNumber(row.sessions)} - - - {row => formatLongNumber(row.visits)} - - - {row => formatLongNumber(row.views)} - - - {row => formatLongNumber(row.events)} - - - {row => - formatShortTime(Math.abs(~~Number(row.totaltime)), ['h', 'm', 's'], ' ') - } - - - - - )} - - - - - - - {sessionTypeChartData && } - - - - ); -} diff --git a/src/app/(main)/websites/[websiteId]/session-data/SessionPropertyChart.tsx b/src/app/(main)/websites/[websiteId]/session-data/SessionPropertyChart.tsx new file mode 100644 index 000000000..b175908df --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/session-data/SessionPropertyChart.tsx @@ -0,0 +1,136 @@ +'use client'; +import { DistributionBarChart } from '@/components/charts/DistributionBarChart'; +import { Empty } from '@/components/common/Empty'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { useMessages, useMobile, useSessionDataActivityStatsQuery } from '@/components/hooks'; +import { formatLongNumber, formatShortTime } from '@/lib/format'; +import type { PropertyFilter } from '@/lib/types'; +import { Column, DataColumn, DataTable, Grid, Row, Text } from '@umami/react-zen'; +import { useMemo } from 'react'; + +export function SessionPropertyChart({ + websiteId, + propertyName, + propertyFilters = [], +}: { + websiteId: string; + propertyName: string; + propertyFilters?: PropertyFilter[]; +}) { + const statsQuery = useSessionDataActivityStatsQuery(websiteId, propertyName, propertyFilters); + const rows = statsQuery.data ?? []; + const { t, labels } = useMessages(); + const { isPhone } = useMobile(); + const activeTable = useMemo(() => rows.filter(row => row.activity > 0).slice(0, 10), [rows]); + const chartData = useMemo( + () => + activeTable.map(row => ({ + label: row.label, + values: { + sessions: row.sessions, + visits: row.visits, + views: row.views, + events: row.events, + }, + })), + [activeTable], + ); + + return ( + + + + {chartData.length ? ( + + {t(labels.activityByProperty)} + row.label)} + datasets={[ + { + label: t(labels.visitors), + values: chartData.map(row => row.values.sessions), + backgroundColor: 'rgba(59, 130, 246, 0.80)', + }, + { + label: t(labels.visits), + values: chartData.map(row => row.values.visits), + backgroundColor: 'rgba(16, 185, 129, 0.80)', + }, + { + label: t(labels.views), + values: chartData.map(row => row.values.views), + backgroundColor: 'rgba(245, 158, 11, 0.80)', + }, + { + label: t(labels.events), + values: chartData.map(row => row.values.events), + backgroundColor: 'rgba(244, 114, 182, 0.80)', + }, + ]} + stacked={true} + /> + + ) : ( + + )} + + + + + {activeTable.length === 0 ? ( + + ) : isPhone ? ( + + + + {row => ( + + + {row.label} + + + )} + + + + ) : ( + + + + + {row => ( + + + {row.label} + + + )} + + + {row => formatLongNumber(row.sessions)} + + + {row => formatLongNumber(row.visits)} + + + {row => formatLongNumber(row.views)} + + + {row => formatLongNumber(row.events)} + + + {row => + formatShortTime(Math.abs(~~Number(row.totaltime)), ['h', 'm', 's'], ' ') + } + + + + + )} + + + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/session-data/SessionStringPropertyView.tsx b/src/app/(main)/websites/[websiteId]/session-data/SessionStringPropertyView.tsx deleted file mode 100644 index e8a623220..000000000 --- a/src/app/(main)/websites/[websiteId]/session-data/SessionStringPropertyView.tsx +++ /dev/null @@ -1,53 +0,0 @@ -'use client'; -import { LoadingPanel } from '@/components/common/LoadingPanel'; -import { useSessionDataStatsQuery } from '@/components/hooks'; -import type { PropertyFilter } from '@/lib/types'; -import { PropertyChart } from '@/components/property-data/PropertyChart'; -import { HighCardinalityPropertyChart } from './HighCardinalityPropertyChart'; - -const HIGH_CARDINALITY_NAME_PATTERN = - /(email|e-mail|name|(^|_)(id|uuid|token)($|_)|distinct_id|user_id|customer_id|client_id|member_id)/i; - -function isHighCardinalityProperty(propertyName: string, totalValues: number, uniqueValues: number) { - if (!totalValues) return false; - - const uniquenessRate = uniqueValues / totalValues; - const hasHighCardinalityName = HIGH_CARDINALITY_NAME_PATTERN.test(propertyName); - - return totalValues >= 20 && (hasHighCardinalityName || uniqueValues >= 20 || uniquenessRate >= 0.6); -} - -export function SessionStringPropertyView({ - websiteId, - propertyName, - propertyFilters = [], -}: { - websiteId: string; - propertyName: string; - propertyFilters?: PropertyFilter[]; -}) { - const statsQuery = useSessionDataStatsQuery(websiteId, propertyName, propertyFilters); - const rows = statsQuery.data ?? []; - const totalValues = rows.reduce((sum, row) => sum + row.sessions, 0); - const uniqueValues = rows.length; - const isHighCardinality = isHighCardinalityProperty( - propertyName, - totalValues, - uniqueValues, - ); - - return ( - - {isHighCardinality ? ( - - ) : ( - - )} - - ); -} diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx index 13f7b1c60..56643d2d8 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx @@ -11,7 +11,7 @@ import { PropertyFilterButton } from '@/components/property-data/PropertyFilterB import { PropertyNumericChart } from '@/components/property-data/PropertyNumericChart'; import { Panel } from '@/components/common/Panel'; import { SessionDataPivotTable } from '../session-data/SessionDataPivotTable'; -import { SessionStringPropertyView } from '../session-data/SessionStringPropertyView'; +import { SessionPropertyChart } from '../session-data/SessionPropertyChart'; export function SessionProperties({ websiteId }: { websiteId: string }) { const [propertyName, setPropertyName] = useState(''); @@ -129,7 +129,7 @@ export function SessionProperties({ websiteId }: { websiteId: string }) { /> )} {propertyName && selectedProperty?.dataType === DATA_TYPE.string && ( - (null); + const { theme } = useTheme(); + const { colors } = useMemo(() => getThemeColors(theme), [theme]); + + const chartData = useMemo(() => { + if (datasets?.length && labels?.length) { + return { + labels, + datasets: datasets.map(dataset => ({ + label: dataset.label, + data: dataset.values, + backgroundColor: dataset.backgroundColor, + borderColor: dataset.borderColor || dataset.backgroundColor, + borderWidth: 1, + })), + }; + } + + return { + labels: (data || []).map(item => item.label), + datasets: [ + { + label: '', + data: (data || []).map(item => item.count), + backgroundColor, + borderColor, + borderWidth: 1, + }, + ], + }; + }, [data, labels, datasets, backgroundColor, borderColor]); + + const chartOptions = useMemo( + () => ({ + indexAxis: horizontal ? ('y' as const) : ('x' as const), + scales: { + x: { + type: horizontal ? ('linear' as const) : ('category' as const), + min: horizontal ? 0 : undefined, + beginAtZero: horizontal ? true : undefined, + stacked, + grid: { + display: horizontal ? true : false, + color: colors.chart.line, + }, + border: { + color: colors.chart.line, + }, + ticks: { + color: colors.chart.text, + autoSkip: horizontal ? undefined : true, + maxRotation: horizontal ? undefined : 0, + callback: horizontal + ? (tickValue: string | number) => renderNumberLabels(String(tickValue)) + : (tickValue: string | number) => chartData.labels?.[Number(tickValue)] || '', + }, + }, + y: { + type: horizontal ? ('category' as const) : ('linear' as const), + min: horizontal ? undefined : 0, + beginAtZero: horizontal ? undefined : true, + stacked, + grid: { + display: horizontal ? false : true, + color: colors.chart.line, + }, + border: { + color: colors.chart.line, + }, + ticks: { + color: colors.chart.text, + callback: horizontal + ? (tickValue: string | number) => chartData.labels?.[Number(tickValue)] || '' + : (tickValue: string | number) => renderNumberLabels(String(tickValue)), + }, + }, + }, + }), + [chartData.labels, colors, horizontal, stacked], + ); + + const handleTooltip = useCallback(({ tooltip }: { tooltip: any }) => { + const { opacity, labelColors, dataPoints } = tooltip; + const point = dataPoints?.[0]; + const nextTooltip = opacity + ? { + title: (point?.label ?? '').toString(), + color: labelColors?.[0]?.backgroundColor, + value: `${formatLongNumber( + Number(horizontal ? point?.raw?.x ?? point?.raw ?? 0 : point?.raw?.y ?? point?.raw ?? 0), + )}${point?.dataset?.label ? ` ${point.dataset.label}` : ''}`, + } + : null; + + setTooltip(prev => { + if ( + prev?.title === nextTooltip?.title && + prev?.color === nextTooltip?.color && + prev?.value === nextTooltip?.value + ) { + return prev; + } + + return nextTooltip; + }); + }, [horizontal]); + + return ( + <> + + {tooltip && } + + ); +} + +export const DistributionBarChart = memo(DistributionBarChartComponent); + +DistributionBarChart.displayName = 'DistributionBarChart'; diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts index bd508317d..a5dfd589f 100644 --- a/src/components/hooks/index.ts +++ b/src/components/hooks/index.ts @@ -47,7 +47,7 @@ export * from './queries/useSessionActivityQuery'; export * from './queries/useSessionDataPropertiesQuery'; export * from './queries/useSessionDataPivotQuery'; export * from './queries/useSessionDataQuery'; -export * from './queries/useSessionDataStatsQuery'; +export * from './queries/useSessionDataActivityStatsQuery'; export * from './queries/useSessionDataValuesQuery'; export * from './queries/useSessionReplaysQuery'; export * from './queries/useShareTokenQuery'; diff --git a/src/components/hooks/queries/useSessionDataStatsQuery.ts b/src/components/hooks/queries/useSessionDataActivityStatsQuery.ts similarity index 91% rename from src/components/hooks/queries/useSessionDataStatsQuery.ts rename to src/components/hooks/queries/useSessionDataActivityStatsQuery.ts index 8380fedf5..72ca11fd5 100644 --- a/src/components/hooks/queries/useSessionDataStatsQuery.ts +++ b/src/components/hooks/queries/useSessionDataActivityStatsQuery.ts @@ -4,7 +4,7 @@ import { useApi } from '../useApi'; import { useDateParameters } from '../useDateParameters'; import { useFilterParameters } from '../useFilterParameters'; -export function useSessionDataStatsQuery( +export function useSessionDataActivityStatsQuery( websiteId: string, propertyName: string, propertyFilters: PropertyFilter[] = [], @@ -16,7 +16,7 @@ export function useSessionDataStatsQuery( return useQuery({ queryKey: [ - 'websites:session-data:stats', + 'websites:session-data:activity-stats', { websiteId, propertyName, propertyFilters, startAt, endAt, unit, timezone, ...params }, ], queryFn: () => diff --git a/src/components/messages.ts b/src/components/messages.ts index 79adab814..72ce903cf 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -191,6 +191,7 @@ export const labels: Record = { filter: 'label.filter', filters: 'label.filters', propertyFilter: 'label.property-filter', + activityByProperty: 'label.activity-by-property', breakdown: 'label.breakdown', true: 'label.true', false: 'label.false', diff --git a/src/lib/types.ts b/src/lib/types.ts index 587f77b96..f512c50bc 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -89,11 +89,6 @@ export interface SessionDataPivotRow { propertyValues: string[]; } -export interface PropertyCardinalityBucket { - label: string; - count: number; -} - export interface PropertyLeaderboardRow { label: string; activity: number; @@ -102,8 +97,6 @@ export interface PropertyLeaderboardRow { views: number; events: number; totaltime: number; - newSessions: number; - returningSessions: number; } export interface QueryOptions { diff --git a/src/queries/sql/index.ts b/src/queries/sql/index.ts index 13800ef3e..380565471 100644 --- a/src/queries/sql/index.ts +++ b/src/queries/sql/index.ts @@ -48,7 +48,7 @@ export * from './sessions/getSessionDataPropertySeries'; export * from './sessions/getSessionDataNumericSeries'; export * from './sessions/getSessionDataNumericStats'; export * from './sessions/getSessionDataPivot'; -export * from './sessions/getSessionDataStats'; +export * from './sessions/getSessionDataActivityStats'; export * from './sessions/getSessionDataValues'; export * from './sessions/getSessionExpandedMetrics'; export * from './sessions/getSessionMetrics'; diff --git a/src/queries/sql/sessions/getSessionDataStats.ts b/src/queries/sql/sessions/getSessionDataActivityStats.ts similarity index 92% rename from src/queries/sql/sessions/getSessionDataStats.ts rename to src/queries/sql/sessions/getSessionDataActivityStats.ts index d0ea361cd..3da98140b 100644 --- a/src/queries/sql/sessions/getSessionDataStats.ts +++ b/src/queries/sql/sessions/getSessionDataActivityStats.ts @@ -4,9 +4,9 @@ import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { PropertyFilter, PropertyLeaderboardRow, QueryFilters } from '@/lib/types'; -const FUNCTION_NAME = 'getSessionDataStats'; +const FUNCTION_NAME = 'getSessionDataActivityStats'; -export async function getSessionDataStats( +export async function getSessionDataActivityStats( ...args: [websiteId: string, propertyName: string, filters: QueryFilters, propertyFilters?: PropertyFilter[]] ): Promise { return runQuery({ @@ -90,9 +90,7 @@ async function relationalQuery( coalesce(sum(session_stats.visits), 0) as visits, coalesce(sum(session_stats.views), 0) as views, coalesce(sum(session_stats.events), 0) as events, - coalesce(sum(session_stats.totaltime), 0) as totaltime, - coalesce(sum(case when session_stats.visits = 1 then 1 else 0 end), 0) as "newSessions", - coalesce(sum(case when session_stats.visits > 1 then 1 else 0 end), 0) as "returningSessions" + coalesce(sum(session_stats.totaltime), 0) as totaltime from property_values join session_stats on session_stats.session_id = property_values.session_id group by property_values.value @@ -170,9 +168,7 @@ async function clickhouseQuery( ifNull(sum(session_stats.visits), 0) as visits, ifNull(sum(session_stats.views), 0) as views, ifNull(sum(session_stats.events), 0) as events, - ifNull(sum(session_stats.totaltime), 0) as totaltime, - ifNull(sum(if(session_stats.visits = 1, 1, 0)), 0) as newSessions, - ifNull(sum(if(session_stats.visits > 1, 1, 0)), 0) as returningSessions + ifNull(sum(session_stats.totaltime), 0) as totaltime from property_values join session_stats on session_stats.session_id = property_values.session_id group by property_values.value diff --git a/src/queries/sql/sessions/getSessionDataPivot.ts b/src/queries/sql/sessions/getSessionDataPivot.ts index 5ce922008..ac4528123 100644 --- a/src/queries/sql/sessions/getSessionDataPivot.ts +++ b/src/queries/sql/sessions/getSessionDataPivot.ts @@ -55,16 +55,28 @@ async function relationalQuery( ${filterQuery} ${pfSQL} ), - selected_property_sessions as ( - select distinct session_data.session_id - from session_data - join filtered_sessions - on filtered_sessions.session_id = session_data.session_id - where session_data.website_id = {{websiteId::uuid}} - and session_data.data_key = {{propertyName}} + latest_session_properties as ( + select + ranked.session_id, + ranked.data_key + from ( + select + session_data.session_id, + session_data.data_key, + row_number() over ( + partition by session_data.session_id, session_data.data_key + order by session_data.created_at desc, session_data.session_data_id desc + ) as row_num + from session_data + join filtered_sessions + on filtered_sessions.session_id = session_data.session_id + where session_data.website_id = {{websiteId::uuid}} + ) ranked + where ranked.row_num = 1 ) select count(*) as num - from selected_property_sessions + from latest_session_properties + where latest_session_properties.data_key = {{propertyName}} `, { ...queryParams, websiteId, propertyName, ...pfParams }, )) as { num: number }[]; @@ -83,14 +95,6 @@ async function relationalQuery( ${filterQuery} ${pfSQL} ), - selected_property_sessions as ( - select distinct session_data.session_id - from session_data - join filtered_sessions - on filtered_sessions.session_id = session_data.session_id - where session_data.website_id = {{websiteId::uuid}} - and session_data.data_key = {{propertyName}} - ), latest_session_properties as ( select ranked.session_id, @@ -116,17 +120,19 @@ async function relationalQuery( order by session_data.created_at desc, session_data.session_data_id desc ) as row_num from session_data - join selected_property_sessions - on selected_property_sessions.session_id = session_data.session_id + join filtered_sessions + on filtered_sessions.session_id = session_data.session_id where session_data.website_id = {{websiteId::uuid}} ) ranked where ranked.row_num = 1 ), paged_sessions as ( - select latest_session_properties.session_id + select + latest_session_properties.session_id, + latest_session_properties.created_at as sort_created_at from latest_session_properties - group by latest_session_properties.session_id - order by max(latest_session_properties.created_at) desc + where latest_session_properties.data_key = {{propertyName}} + order by latest_session_properties.created_at desc limit ${size} offset ${offset} ) select @@ -146,9 +152,10 @@ async function relationalQuery( order by latest_session_properties.data_key asc ) as "propertyValues" from latest_session_properties - join paged_sessions on paged_sessions.session_id = latest_session_properties.session_id - group by latest_session_properties.session_id - order by max(latest_session_properties.created_at) desc + join paged_sessions + on paged_sessions.session_id = latest_session_properties.session_id + group by latest_session_properties.session_id, paged_sessions.sort_created_at + order by paged_sessions.sort_created_at desc `, { ...queryParams, websiteId, propertyName, ...pfParams }, FUNCTION_NAME, @@ -187,15 +194,21 @@ async function clickhouseQuery( ${filterQuery} ${pfSQL} ), - selected_property_sessions as ( - select distinct session_data.session_id + latest_session_properties as ( + select + session_data.session_id as session_id, + session_data.data_key as data_key from session_data final - join filtered_sessions on filtered_sessions.session_id = session_data.session_id + join filtered_sessions + on filtered_sessions.session_id = session_data.session_id where session_data.website_id = {websiteId:UUID} - and session_data.data_key = {propertyName:String} + group by + session_data.session_id, + session_data.data_key ) select count() as num - from selected_property_sessions + from latest_session_properties + where latest_session_properties.data_key = {propertyName:String} `, { ...queryParams, websiteId, propertyName, ...pfParams }, )) as { num: number }[]; @@ -212,13 +225,6 @@ async function clickhouseQuery( ${filterQuery} ${pfSQL} ), - selected_property_sessions as ( - select distinct session_data.session_id - from session_data final - join filtered_sessions on filtered_sessions.session_id = session_data.session_id - where session_data.website_id = {websiteId:UUID} - and session_data.data_key = {propertyName:String} - ), latest_session_properties as ( select session_data.session_id as session_id, @@ -230,8 +236,8 @@ async function clickhouseQuery( argMax(session_data.date_value, session_data.created_at) as date_value, max(session_data.created_at) as created_at from session_data final - join selected_property_sessions - on selected_property_sessions.session_id = session_data.session_id + join filtered_sessions + on filtered_sessions.session_id = session_data.session_id where session_data.website_id = {websiteId:UUID} group by session_data.session_id, @@ -239,10 +245,11 @@ async function clickhouseQuery( ), paged_sessions as ( select - latest_session_properties.session_id + latest_session_properties.session_id, + latest_session_properties.created_at as sort_created_at from latest_session_properties - group by latest_session_properties.session_id - order by max(latest_session_properties.created_at) desc + where latest_session_properties.data_key = {propertyName:String} + order by latest_session_properties.created_at desc limit ${size} offset ${offset} ) select @@ -262,8 +269,9 @@ async function clickhouseQuery( join paged_sessions on paged_sessions.session_id = latest_session_properties.session_id group by - latest_session_properties.session_id - order by createdAt desc + latest_session_properties.session_id, + paged_sessions.sort_created_at + order by paged_sessions.sort_created_at desc `, { ...queryParams, websiteId, propertyName, ...pfParams }, FUNCTION_NAME, diff --git a/src/queries/sql/sessions/getSessionDataValues.ts b/src/queries/sql/sessions/getSessionDataValues.ts index e015e30ef..1c7434768 100644 --- a/src/queries/sql/sessions/getSessionDataValues.ts +++ b/src/queries/sql/sessions/getSessionDataValues.ts @@ -31,7 +31,7 @@ async function relationalQuery( ` select array_item.value as "value", - count(distinct session_data.session_id) as "total" + count(*) as "total" from website_event ${cohortQuery} ${joinSessionQuery} @@ -46,7 +46,7 @@ async function relationalQuery( ${filterQuery} group by array_item.value order by 2 desc - limit 100 + limit 500 `, queryParams, FUNCTION_NAME, @@ -61,7 +61,7 @@ async function relationalQuery( when data_type = 4 then ${getDateSQL('date_value', 'hour')} else string_value end as "value", - count(distinct session_data.session_id) as "total" + count(*) as "total" from website_event ${cohortQuery} ${joinSessionQuery} @@ -75,7 +75,7 @@ async function relationalQuery( ${filterQuery} group by value order by 2 desc - limit 100 + limit 500 `, queryParams, FUNCTION_NAME, @@ -95,7 +95,7 @@ async function clickhouseQuery( ` select arrayJoin(JSONExtract(ifNull(session_data.string_value, '[]'), 'Array(String)')) as "value", - uniq(session_data.session_id) as "total" + count() as "total" from website_event ${cohortQuery} join session_data final @@ -108,7 +108,7 @@ async function clickhouseQuery( ${filterQuery} group by value order by 2 desc - limit 100 + limit 500 `, queryParams, FUNCTION_NAME, @@ -121,7 +121,7 @@ async function clickhouseQuery( multiIf(data_type = 2, replaceAll(string_value, '.0000', ''), data_type = 4, toString(date_trunc('hour', date_value)), string_value) as "value", - uniq(session_data.session_id) as "total" + count() as "total" from website_event ${cohortQuery} join session_data final @@ -134,7 +134,7 @@ async function clickhouseQuery( ${filterQuery} group by value order by 2 desc - limit 100 + limit 500 `, queryParams, FUNCTION_NAME, From 00c0273500c671c169bb6368235d4e25c4b482bb Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 1 May 2026 13:16:30 -0700 Subject: [PATCH 57/59] fix clickhouse filtering on event_name Co-authored-by: Copilot --- pnpm-lock.yaml | 166 ------------------ .../[websiteId]/event-data/values/route.ts | 4 +- .../sql/events/getEventDataArraySeries.ts | 7 +- .../sql/events/getEventDataDateSeries.ts | 9 +- .../sql/events/getEventDataNumericSeries.ts | 23 ++- .../sql/events/getEventDataNumericStats.ts | 7 +- src/queries/sql/events/getEventDataPivot.ts | 42 ++++- .../sql/events/getEventDataPropertySeries.ts | 7 +- src/queries/sql/events/getEventDataValues.ts | 42 +++-- 9 files changed, 108 insertions(+), 199 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 616a54142..b538e3050 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -313,45 +313,6 @@ importers: specifier: ^6.0.3 version: 6.0.3 - dist: - dependencies: - chart.js: - specifier: ^4.5.0 - version: 4.5.1 - chartjs-adapter-date-fns: - specifier: ^3.0.0 - version: 3.0.0(chart.js@4.5.1)(date-fns@4.1.0) - colord: - specifier: ^2.9.2 - version: 2.9.3 - jsonwebtoken: - specifier: ^9.0.2 - version: 9.0.3 - lucide-react: - specifier: ^0.542.0 - version: 0.542.0(react@19.2.5) - pure-rand: - specifier: ^7.0.1 - version: 7.0.1 - react-simple-maps: - specifier: ^2.3.0 - version: 2.3.0(prop-types@15.8.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react-use-measure: - specifier: ^2.0.4 - version: 2.1.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react-window: - specifier: ^1.8.6 - version: 1.8.11(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - serialize-error: - specifier: ^12.0.0 - version: 12.0.0 - thenby: - specifier: ^1.3.4 - version: 1.3.4 - uuid: - specifier: ^11.1.0 - version: 11.1.0 - packages: '@ampproject/remapping@2.3.0': @@ -551,10 +512,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.28.3': - resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.29.2': resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} @@ -608,28 +565,24 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [musl] '@biomejs/cli-linux-arm64@2.4.12': resolution: {integrity: sha512-tOwuCuZZtKi1jVzbk/5nXmIsziOB6yqN8c9r9QM0EJYPU6DpQWf11uBOSCfFKKM4H3d9ZoarvlgMfbcuD051Pw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [glibc] '@biomejs/cli-linux-x64-musl@2.4.12': resolution: {integrity: sha512-dwTIgZrGutzhkQCuvHynCkyW6hJxUuyZqKKO0YNfaS2GUoRO+tOvxXZqZB6SkWAOdfZTzwaw8IEdUnIkHKHoew==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [musl] '@biomejs/cli-linux-x64@2.4.12': resolution: {integrity: sha512-8pFeAnLU9QdW9jCIslB/v82bI0lhBmz2ZAKc8pVMFPO0t0wAHsoEkrUQUbMkIorTRIjbqyNZHA3lEXavsPWYSw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [glibc] '@biomejs/cli-win32-arm64@2.4.12': resolution: {integrity: sha512-B0DLnx0vA9ya/3v7XyCaP+/lCpnbWbMOfUFFve+xb5OxyYvdHaS55YsSddr228Y+JAFk58agCuZTsqNiw2a6ig==} @@ -1573,105 +1526,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -1873,28 +1810,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@16.2.4': resolution: {integrity: sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@16.2.4': resolution: {integrity: sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@16.2.4': resolution: {integrity: sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@16.2.4': resolution: {integrity: sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==} @@ -1949,42 +1882,36 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@parcel/watcher-win32-arm64@2.5.6': resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} @@ -2382,79 +2309,66 @@ packages: resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.1': resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.1': resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.1': resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.1': resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.1': resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.1': resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.1': resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.1': resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.1': resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.1': resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.1': resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.1': resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.60.1': resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} @@ -2616,42 +2530,36 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [glibc] '@swc/core-linux-arm64-musl@1.15.26': resolution: {integrity: sha512-iNlbvTIo425rkKzDLLWFJGnFXr3myETUdIDHcjuiPNZE8b0ogmcAuilC4yEJX7FSHGbnlsoJcCT2xf4b3VJmmQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [musl] '@swc/core-linux-ppc64-gnu@1.15.26': resolution: {integrity: sha512-AuuEOtG+YXKIjIUup4RsxYNklx6XVB3WKWfhxG6hnfPrn7vp89RNOLbbyyprgj6Sk7k9ulwGVTJElEvmBNPSCA==} engines: {node: '>=10'} cpu: [ppc64] os: [linux] - libc: [glibc] '@swc/core-linux-s390x-gnu@1.15.26': resolution: {integrity: sha512-JcMDWQvW1BchUyRg8E0jHiTx7CQYpUr5uDEL1dnPDECrEjBEGG2ynmJ3XX70sWXql0JagqR1t3VpANYFWdUnqA==} engines: {node: '>=10'} cpu: [s390x] os: [linux] - libc: [glibc] '@swc/core-linux-x64-gnu@1.15.26': resolution: {integrity: sha512-FW7V7Mbpq4+PA7BiAq76LJs8MdNuUSylyuRVfQRkhIyeWadFroZ+KOPgjku8Z/fXzngxBRvsk+PGGB0t8mGcjA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [glibc] '@swc/core-linux-x64-musl@1.15.26': resolution: {integrity: sha512-w8erqMHsVcdGwUfJxF6LaiTuPoKnyLOcUbhLcxiXrlLt5MLjtlgcIeUY/NWK/oPoyqkgH+/i8pOJnMTxvl83ZQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [musl] '@swc/core-win32-arm64-msvc@1.15.26': resolution: {integrity: sha512-uDCWCNpUiqkbvPmsuPUTn/P7ag9SqNXD2JT/W3dUu7yZ2krzN+nmmoQ2xRX63/J6RYiHI7aT4jo7Z++lsljlPA==} @@ -2864,49 +2772,41 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -4786,11 +4686,6 @@ packages: resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} - lucide-react@0.542.0: - resolution: {integrity: sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==} - peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - lucide-react@0.555.0: resolution: {integrity: sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA==} peerDependencies: @@ -4831,9 +4726,6 @@ packages: mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} - memoize-one@5.2.1: - resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} - memorystream@0.3.1: resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} engines: {node: '>= 0.10.0'} @@ -5843,13 +5735,6 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - react-simple-maps@2.3.0: - resolution: {integrity: sha512-IZVeiPSRZKwD6I/2NvXpQ2uENYGDGZp8DvZjkapcxuJ/LQHTfl+Byb+KNgY7s+iatRA2ad8LnZ3AgqcjziCCsw==} - peerDependencies: - prop-types: ^15.7.2 - react: ^16.8.0 || 17.x - react-dom: ^16.8.0 || 17.x - react-simple-maps@3.0.0: resolution: {integrity: sha512-vKNFrvpPG8Vyfdjnz5Ne1N56rZlDfHXv5THNXOVZMqbX1rWZA48zQuYT03mx6PAKanqarJu/PDLgshIZAfHHqw==} peerDependencies: @@ -5871,13 +5756,6 @@ packages: react-dom: optional: true - react-window@1.8.11: - resolution: {integrity: sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==} - engines: {node: '>8.0.0'} - peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-window@2.2.7: resolution: {integrity: sha512-SH5nvfUQwGHYyriDUAOt7wfPsfG9Qxd6OdzQxl5oQ4dsSsUicqQvjV7dR+NqZ4coY0fUn3w1jnC5PwzIUWEg5w==} peerDependencies: @@ -6083,10 +5961,6 @@ packages: seq-queue@0.0.5: resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} - serialize-error@12.0.0: - resolution: {integrity: sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw==} - engines: {node: '>=18'} - serialize-error@13.0.1: resolution: {integrity: sha512-bBZaRwLH9PN5HbLCjPId4dP5bNGEtumcErgOX952IsvOhVPrm3/AeK1y0UHA/QaPG701eg0yEnOKsCOC6X/kaA==} engines: {node: '>=20'} @@ -6401,9 +6275,6 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} - thenby@1.3.4: - resolution: {integrity: sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==} - thenby@1.4.0: resolution: {integrity: sha512-D+BkEl5MpeTbQ1Rz0io/TLZmwmYUAgvUpS1r2eKUvPicYOnpqzjZE6bVa7rSEYv189zkSM+CVc+hG5X7nVe9LQ==} @@ -6662,10 +6533,6 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - uuid@11.1.0: - resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} - hasBin: true - uuid@13.0.0: resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} hasBin: true @@ -7063,8 +6930,6 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/runtime@7.28.3': {} - '@babel/runtime@7.29.2': {} '@babel/template@7.27.2': @@ -11409,10 +11274,6 @@ snapshots: lru.min@1.1.4: {} - lucide-react@0.542.0(react@19.2.5): - dependencies: - react: 19.2.5 - lucide-react@0.555.0(react@19.2.5): dependencies: react: 19.2.5 @@ -11448,8 +11309,6 @@ snapshots: mdn-data@2.0.30: {} - memoize-one@5.2.1: {} - memorystream@0.3.1: {} merge-stream@2.0.0: {} @@ -12470,16 +12329,6 @@ snapshots: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - react-simple-maps@2.3.0(prop-types@15.8.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): - dependencies: - d3-geo: 2.0.2 - d3-selection: 2.0.0 - d3-zoom: 2.0.0 - prop-types: 15.8.1 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - topojson-client: 3.1.0 - react-simple-maps@3.0.0(prop-types@15.8.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: d3-geo: 2.0.2 @@ -12506,13 +12355,6 @@ snapshots: optionalDependencies: react-dom: 19.2.5(react@19.2.5) - react-window@1.8.11(react-dom@19.2.5(react@19.2.5))(react@19.2.5): - dependencies: - '@babel/runtime': 7.28.3 - memoize-one: 5.2.1 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - react-window@2.2.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: react: 19.2.5 @@ -12774,10 +12616,6 @@ snapshots: seq-queue@0.0.5: {} - serialize-error@12.0.0: - dependencies: - type-fest: 4.41.0 - serialize-error@13.0.1: dependencies: non-error: 0.1.0 @@ -13142,8 +12980,6 @@ snapshots: glob: 7.2.3 minimatch: 3.1.5 - thenby@1.3.4: {} - thenby@1.4.0: {} thenify-all@1.6.0: @@ -13421,8 +13257,6 @@ snapshots: util-deprecate@1.0.2: {} - uuid@11.1.0: {} - uuid@13.0.0: {} uuid@8.3.2: {} diff --git a/src/app/api/websites/[websiteId]/event-data/values/route.ts b/src/app/api/websites/[websiteId]/event-data/values/route.ts index 4f1d8fc62..b0843427b 100644 --- a/src/app/api/websites/[websiteId]/event-data/values/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/values/route.ts @@ -30,10 +30,10 @@ export async function GET( return unauthorized(); } - const { propertyName, dataType } = query; + const { event: eventName, propertyName, dataType } = query; const filters = await getQueryFilters(query, websiteId); - const data = await getEventDataValues(websiteId, { + const data = await getEventDataValues(websiteId, eventName, { ...filters, propertyName, dataType, diff --git a/src/queries/sql/events/getEventDataArraySeries.ts b/src/queries/sql/events/getEventDataArraySeries.ts index 6d3e3989c..8e06e906c 100644 --- a/src/queries/sql/events/getEventDataArraySeries.ts +++ b/src/queries/sql/events/getEventDataArraySeries.ts @@ -75,7 +75,11 @@ async function clickhouseQuery( ): Promise { const { timezone = 'UTC', unit = 'day' } = filters; const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = clickhouse; - const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { filterQuery, cohortQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + timezone, + }); const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); return rawQuery( @@ -98,6 +102,7 @@ async function clickhouseQuery( ${cohortQuery} where event_data.website_id = {websiteId:UUID} and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_data.event_name = {eventName:String} and event_data.data_key = {propertyName:String} and event_data.data_type = ${DATA_TYPE.array} ${filterQuery} diff --git a/src/queries/sql/events/getEventDataDateSeries.ts b/src/queries/sql/events/getEventDataDateSeries.ts index 26719dbc3..ac4ebff26 100644 --- a/src/queries/sql/events/getEventDataDateSeries.ts +++ b/src/queries/sql/events/getEventDataDateSeries.ts @@ -27,7 +27,7 @@ async function relationalQuery( propertyName: string, filters: QueryFilters, eventFilters: EventPropertyFilter[] = [], -) : Promise { +): Promise { const { timezone = 'utc' } = filters; const { rawQuery, parseFilters, getDateStringSQL, getPropertyFilterQuery } = prisma; const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ @@ -73,7 +73,11 @@ async function clickhouseQuery( ): Promise { const { timezone = 'UTC' } = filters; const { rawQuery, parseFilters, getDateStringSQL, getPropertyFilterQuery } = clickhouse; - const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { filterQuery, cohortQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + timezone, + }); const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); return rawQuery( @@ -95,6 +99,7 @@ async function clickhouseQuery( ${cohortQuery} where event_data.website_id = {websiteId:UUID} and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_data.event_name = {eventName:String} and event_data.data_key = {propertyName:String} and event_data.data_type = ${DATA_TYPE.date} ${filterQuery} diff --git a/src/queries/sql/events/getEventDataNumericSeries.ts b/src/queries/sql/events/getEventDataNumericSeries.ts index 1fca6c0f6..d9df94f54 100644 --- a/src/queries/sql/events/getEventDataNumericSeries.ts +++ b/src/queries/sql/events/getEventDataNumericSeries.ts @@ -38,9 +38,11 @@ async function relationalQuery( }); const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); const aggSql = - metric === 'avg' ? 'avg(cast(event_data.number_value as decimal))' : - metric === 'count' ? 'count(*)' : - 'sum(cast(event_data.number_value as decimal))'; + metric === 'avg' + ? 'avg(cast(event_data.number_value as decimal))' + : metric === 'count' + ? 'count(*)' + : 'sum(cast(event_data.number_value as decimal))'; return rawQuery( ` @@ -79,12 +81,18 @@ async function clickhouseQuery( ): Promise<{ t: string; y: number }[]> { const { timezone = 'UTC', unit = 'day' } = filters; const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = clickhouse; - const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { filterQuery, cohortQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + timezone, + }); const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); const aggSql = - metric === 'avg' ? 'avg(event_data.number_value)' : - metric === 'count' ? 'count()' : - 'sum(event_data.number_value)'; + metric === 'avg' + ? 'avg(event_data.number_value)' + : metric === 'count' + ? 'count()' + : 'sum(event_data.number_value)'; return rawQuery( ` @@ -105,6 +113,7 @@ async function clickhouseQuery( ${cohortQuery} where event_data.website_id = {websiteId:UUID} and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_data.event_name = {eventName:String} and event_data.data_key = {propertyName:String} and event_data.data_type = 2 ${filterQuery} diff --git a/src/queries/sql/events/getEventDataNumericStats.ts b/src/queries/sql/events/getEventDataNumericStats.ts index 90896d989..5977ff3ac 100644 --- a/src/queries/sql/events/getEventDataNumericStats.ts +++ b/src/queries/sql/events/getEventDataNumericStats.ts @@ -73,7 +73,11 @@ async function clickhouseQuery( ): Promise { const { timezone = 'UTC' } = filters; const { rawQuery, parseFilters, getPropertyFilterQuery } = clickhouse; - const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { filterQuery, cohortQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + timezone, + }); const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); return rawQuery( @@ -98,6 +102,7 @@ async function clickhouseQuery( ${cohortQuery} where event_data.website_id = {websiteId:UUID} and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_data.event_name = {eventName:String} and event_data.data_key = {propertyName:String} and event_data.data_type = 2 ${filterQuery} diff --git a/src/queries/sql/events/getEventDataPivot.ts b/src/queries/sql/events/getEventDataPivot.ts index 6f7551aed..8699d3269 100644 --- a/src/queries/sql/events/getEventDataPivot.ts +++ b/src/queries/sql/events/getEventDataPivot.ts @@ -20,7 +20,12 @@ export async function getEventDataPivot( }); } -async function relationalQuery(websiteId: string, eventName: string, filters: QueryFilters, eventFilters: EventPropertyFilter[] = []) { +async function relationalQuery( + websiteId: string, + eventName: string, + filters: QueryFilters, + eventFilters: EventPropertyFilter[] = [], +) { const { timezone = 'utc' } = filters; const { rawQuery, parseFilters, getPropertyFilterQuery, getDateStringSQL } = prisma; const { page = 1, pageSize } = filters; @@ -104,11 +109,27 @@ async function relationalQuery(websiteId: string, eventName: string, filters: Qu // Pivot flat rows into one record per event const eventMap = new Map< string, - { eventId: string; sessionId: string; eventName: string; urlPath: string; createdAt: Date; propertyKeys: string[]; propertyValues: string[] } + { + eventId: string; + sessionId: string; + eventName: string; + urlPath: string; + createdAt: Date; + propertyKeys: string[]; + propertyValues: string[]; + } >(); for (const { eventId, sessionId, eventName: name, urlPath, createdAt, dataKey, value } of rows) { if (!eventMap.has(eventId)) { - eventMap.set(eventId, { eventId, sessionId, eventName: name, urlPath, createdAt, propertyKeys: [], propertyValues: [] }); + eventMap.set(eventId, { + eventId, + sessionId, + eventName: name, + urlPath, + createdAt, + propertyKeys: [], + propertyValues: [], + }); } const entry = eventMap.get(eventId); entry.propertyKeys.push(dataKey); @@ -118,14 +139,23 @@ async function relationalQuery(websiteId: string, eventName: string, filters: Qu return { data: [...eventMap.values()], count, page: +page, pageSize: size }; } -async function clickhouseQuery(websiteId: string, eventName: string, filters: QueryFilters, eventFilters: EventPropertyFilter[] = []) { +async function clickhouseQuery( + websiteId: string, + eventName: string, + filters: QueryFilters, + eventFilters: EventPropertyFilter[] = [], +) { const { timezone = 'UTC' } = filters; const { rawQuery, parseFilters, getPropertyFilterQuery, getDateStringSQL } = clickhouse; const { page = 1, pageSize } = filters; const size = +pageSize || DEFAULT_PAGE_SIZE; const offset = +size * (+page - 1); - const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { filterQuery, cohortQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + timezone, + }); const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); const count = await rawQuery( @@ -145,6 +175,7 @@ async function clickhouseQuery(websiteId: string, eventName: string, filters: Qu ${cohortQuery} where event_data_pivot.website_id = {websiteId:UUID} and event_data_pivot.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_data_pivot.event_name = {eventName:String} ${filterQuery} ${pfSQL} `, @@ -183,6 +214,7 @@ async function clickhouseQuery(websiteId: string, eventName: string, filters: Qu ${cohortQuery} where event_data_pivot.website_id = {websiteId:UUID} and event_data_pivot.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_data_pivot.event_name = {eventName:String} ${filterQuery} ${pfSQL} group by diff --git a/src/queries/sql/events/getEventDataPropertySeries.ts b/src/queries/sql/events/getEventDataPropertySeries.ts index b2de7d379..7cc7071c6 100644 --- a/src/queries/sql/events/getEventDataPropertySeries.ts +++ b/src/queries/sql/events/getEventDataPropertySeries.ts @@ -74,7 +74,11 @@ async function clickhouseQuery( ): Promise { const { timezone = 'UTC', unit = 'day' } = filters; const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = clickhouse; - const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); + const { filterQuery, cohortQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + timezone, + }); const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); return rawQuery( @@ -97,6 +101,7 @@ async function clickhouseQuery( ${cohortQuery} where event_data.website_id = {websiteId:UUID} and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_data.event_name = {eventName:String} and event_data.data_key = {propertyName:String} and event_data.data_type in (${DATA_TYPE.string}, ${DATA_TYPE.boolean}) ${filterQuery} diff --git a/src/queries/sql/events/getEventDataValues.ts b/src/queries/sql/events/getEventDataValues.ts index 8196ff5ce..f4d0349bd 100644 --- a/src/queries/sql/events/getEventDataValues.ts +++ b/src/queries/sql/events/getEventDataValues.ts @@ -12,7 +12,11 @@ interface WebsiteEventData { } export async function getEventDataValues( - ...args: [websiteId: string, filters: QueryFilters & { propertyName?: string; dataType?: number }] + ...args: [ + websiteId: string, + eventName: string, + filters: QueryFilters & { propertyName?: string; dataType?: number }, + ] ): Promise { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -22,6 +26,7 @@ export async function getEventDataValues( async function relationalQuery( websiteId: string, + eventName: string, filters: QueryFilters & { propertyName?: string; dataType?: number }, ) { const { rawQuery, parseFilters, getDateSQL } = prisma; @@ -41,6 +46,8 @@ async function relationalQuery( join website_event on website_event.event_id = event_data.website_event_id and website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_type = 2 + and website_event.event_name = {{eventName}} cross join lateral jsonb_array_elements_text(coalesce(event_data.string_value, '[]')::jsonb) as array_item(value) ${cohortQuery} ${joinSessionQuery} @@ -53,7 +60,7 @@ async function relationalQuery( order by 2 desc limit 100 `, - queryParams, + { ...queryParams, eventName }, FUNCTION_NAME, ); } @@ -61,9 +68,9 @@ async function relationalQuery( return rawQuery( ` select - case - when data_type = 2 then replace(string_value, '.0000', '') - when data_type = 4 then ${getDateSQL('date_value', 'hour')} + case + when data_type = 2 then replace(string_value, '.0000', '') + when data_type = 4 then ${getDateSQL('date_value', 'hour')} else string_value end as "value", count(*) as "total" @@ -71,6 +78,8 @@ async function relationalQuery( join website_event on website_event.event_id = event_data.website_event_id and website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_type = 2 + and website_event.event_name = {eventName:String} ${cohortQuery} ${joinSessionQuery} where event_data.website_id = {{websiteId::uuid}} @@ -82,13 +91,14 @@ async function relationalQuery( order by 2 desc limit 100 `, - queryParams, + { ...queryParams, eventName }, FUNCTION_NAME, ); } async function clickhouseQuery( websiteId: string, + eventName: string, filters: QueryFilters & { propertyName?: string; dataType?: number }, ): Promise<{ value: string; total: number }[]> { const { rawQuery, parseFilters } = clickhouse; @@ -101,19 +111,21 @@ async function clickhouseQuery( select arrayJoin(JSONExtract(ifNull(event_data.string_value, '[]'), 'Array(String)')) as "value", count(*) as "total" - from event_data + from event_data any left join ( - select * + select * from website_event where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type = 2) website_event + and event_type = 2 + and event_name = {eventName:String}) website_event on website_event.event_id = event_data.event_id and website_event.session_id = event_data.session_id and website_event.website_id = event_data.website_id ${cohortQuery} where event_data.website_id = {websiteId:UUID} and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_data.event_name = {eventName:String} and event_data.data_key = {propertyName:String} and event_data.data_type = ${DATA_TYPE.array} ${filterQuery} @@ -121,7 +133,7 @@ async function clickhouseQuery( order by 2 desc limit 100 `, - queryParams, + { ...queryParams, eventName }, FUNCTION_NAME, ); } @@ -133,19 +145,21 @@ async function clickhouseQuery( data_type = 4, toString(date_trunc('hour', date_value)), string_value) as "value", count(*) as "total" - from event_data + from event_data any left join ( - select * + select * from website_event where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type = 2) website_event + and event_type = 2 + and event_name = {eventName:String}) website_event on website_event.event_id = event_data.event_id and website_event.session_id = event_data.session_id and website_event.website_id = event_data.website_id ${cohortQuery} where event_data.website_id = {websiteId:UUID} and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_data.event_name = {eventName:String} and event_data.data_key = {propertyName:String} ${dataType ? `and event_data.data_type = ${dataType}` : ''} ${filterQuery} @@ -153,7 +167,7 @@ async function clickhouseQuery( order by 2 desc limit 100 `, - queryParams, + { ...queryParams, eventName }, FUNCTION_NAME, ); } From 946a969df67cb540e320f9091f5366ae96206c58 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 1 May 2026 13:25:54 -0700 Subject: [PATCH 58/59] fix syntax error --- src/queries/sql/events/getEventDataValues.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/queries/sql/events/getEventDataValues.ts b/src/queries/sql/events/getEventDataValues.ts index f4d0349bd..70e134e5b 100644 --- a/src/queries/sql/events/getEventDataValues.ts +++ b/src/queries/sql/events/getEventDataValues.ts @@ -79,7 +79,7 @@ async function relationalQuery( and website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} and website_event.event_type = 2 - and website_event.event_name = {eventName:String} + and website_event.event_name = {{eventName}} ${cohortQuery} ${joinSessionQuery} where event_data.website_id = {{websiteId::uuid}} From 62c214d7c5db4cfd81b5bde0fbeb5b858a10ace9 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 1 May 2026 15:34:07 -0700 Subject: [PATCH 59/59] Add CONTRIBUTING.md. --- CONTRIBUTING.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..623e689e1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,46 @@ +# Contributing to Umami + +Thanks for your interest in contributing to Umami! This document outlines the process for contributing code. + +## Branching + +Umami uses the following long-lived branches: + +- `master` — stable, released code. **Do not open PRs against `master`.** +- `dev` — active development. **All pull requests should target `dev`.** + +Feature branches and fixes are merged into `dev`, and `dev` is periodically merged into `master` for releases. + +## Submitting a Pull Request + +1. Fork the repository and create your branch from `dev`: + ```bash + git checkout dev + git pull origin dev + git checkout -b my-feature + ``` +2. Make your changes. Keep PRs focused — one logical change per PR. +3. Ensure the project builds and lints cleanly: + ```bash + pnpm install + pnpm build + pnpm lint + ``` +4. Push your branch and open a pull request **against the `dev` branch**. +5. Fill in the PR description with what changed and why. Link any related issues. + +PRs opened against `master` will be asked to retarget `dev`. + +## Reporting Issues + +- Search [existing issues](https://github.com/umami-software/umami/issues) before opening a new one. +- For bugs, include reproduction steps, expected vs. actual behavior, and your environment (Umami version, database, browser). +- For feature requests, describe the use case before the proposed solution. + +## Development Setup + +See the [README](./README.md) for instructions on installing dependencies, configuring the database, and running Umami locally. + +## License + +By contributing, you agree that your contributions will be licensed under the [MIT License](./LICENSE).