Compare commits

...

8 Commits

Author SHA1 Message Date
Theodór Tómas
f49f40610b feat: add Cube.js dev setup and analytics client (#7287) 2026-02-18 21:10:52 +07:00
Theodór Tómas
9e754bad9c feat: add Chart, Dashboard, DashboardWidget schema and migration (#7286) 2026-02-18 21:10:36 +07:00
Dhruwang
4dcf6fda40 fix: code rabbit feedback 2026-02-18 18:44:24 +05:30
Dhruwang
1b8ccd7199 feat: add JSON type definitions for Chart and Dashboard fields
Add Zod schemas and TypeScript types for ChartQuery, ChartConfig,
WidgetLayout. ChartQuery mirrors Cube.js REST API query format.
Register types with prisma-json-types-generator.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-18 18:17:44 +05:30
Dhruwang
4f9088559f feat: add Cube.js dev setup and analytics client
- Add Cube container to docker-compose.dev.yml (pinned v1.3.21)
- Add Cube server config (cube/cube.js) and FeedbackRecords schema
- Add @cubejs-client/core dependency and singleton client in EE module
- Add CUBEJS_API_URL and CUBEJS_API_TOKEN to .env.example

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-18 18:05:47 +05:30
Dhruwang
18550f1d11 feat: link Chart and Dashboard createdBy to User
- Add creator relation on Chart and Dashboard to User
- Add createdBy foreign key constraints in migration (ON DELETE SET NULL)
- Mirror Survey pattern for createdBy user tracking

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-18 17:26:42 +05:30
Dhruwang
881cd31f74 feat: add Chart, Dashboard, DashboardWidget schema and migration
- Add Prisma models for Chart, Dashboard, DashboardWidget
- ChartType: area, bar, line, pie, big_number only
- Remove DashboardStatus and WidgetType (widgets are always charts)
- DashboardWidget requires chartId, remove content/type fields

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-18 17:24:48 +05:30
Dhruwang
e00405dca2 feat: add Chart, Dashboard, and DashboardWidget schema and migration
- Add Prisma models for Chart, Dashboard, DashboardWidget
- Add ChartType, DashboardStatus, WidgetType enums
- Add migration 20260128111722 for charts and dashboards tables

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-18 17:21:34 +05:30
13 changed files with 570 additions and 17 deletions

View File

@@ -229,5 +229,14 @@ REDIS_URL=redis://localhost:6379
# AUDIT_LOG_GET_USER_IP=0
# Cube.js Analytics (optional — only needed for the analytics/dashboard feature)
# Required when running the Cube service (docker-compose.dev.yml). Generate with: openssl rand -hex 32
# Use the same value for CUBEJS_API_TOKEN so the client can authenticate.
# CUBEJS_API_SECRET=
# URL where the Cube.js instance is running
# CUBEJS_API_URL=http://localhost:4000
# API token sent with each Cube.js request; must match CUBEJS_API_SECRET when CUBEJS_DEV_MODE is off
# CUBEJS_API_TOKEN=
# Lingo.dev API key for translation generation
LINGODOTDEV_API_KEY=your_api_key_here

View File

@@ -0,0 +1,29 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { executeQuery } from "./cube-client";
const mockLoad = vi.fn();
const mockTablePivot = vi.fn();
vi.mock("@cubejs-client/core", () => ({
default: vi.fn(() => ({
load: mockLoad,
})),
}));
describe("executeQuery", () => {
beforeEach(() => {
vi.clearAllMocks();
const resultSet = { tablePivot: mockTablePivot };
mockLoad.mockResolvedValue(resultSet);
mockTablePivot.mockReturnValue([{ id: "1", count: 42 }]);
});
test("loads query and returns tablePivot result", async () => {
const query = { measures: ["FeedbackRecords.count"] };
const result = await executeQuery(query);
expect(mockLoad).toHaveBeenCalledWith(query);
expect(mockTablePivot).toHaveBeenCalled();
expect(result).toEqual([{ id: "1", count: 42 }]);
});
});

View File

@@ -0,0 +1,25 @@
import cubejs, { type CubeApi, type Query } from "@cubejs-client/core";
const getApiUrl = (): string => {
const baseUrl = process.env.CUBEJS_API_URL || "http://localhost:4000";
if (baseUrl.includes("/cubejs-api/v1")) {
return baseUrl;
}
return `${baseUrl.replace(/\/$/, "")}/cubejs-api/v1`;
};
let cubeClient: CubeApi | null = null;
function getCubeClient(): CubeApi {
if (!cubeClient) {
const token = process.env.CUBEJS_API_TOKEN ?? "";
cubeClient = cubejs(token, { apiUrl: getApiUrl() });
}
return cubeClient;
}
export async function executeQuery(query: Query) {
const client = getCubeClient();
const resultSet = await client.load(query);
return resultSet.tablePivot();
}

View File

@@ -23,6 +23,7 @@
"@aws-sdk/s3-presigned-post": "3.971.0",
"@aws-sdk/s3-request-presigner": "3.971.0",
"@boxyhq/saml-jackson": "1.52.2",
"@cubejs-client/core": "1.6.6",
"@dnd-kit/core": "6.3.1",
"@dnd-kit/modifiers": "9.0.0",
"@dnd-kit/sortable": "10.0.0",

12
cube/cube.js Normal file
View File

@@ -0,0 +1,12 @@
module.exports = {
// queryRewrite runs before every Cube query. Use it to enforce row-level security (RLS)
// by injecting filters based on the caller's identity (e.g. organizationId, projectId).
//
// The securityContext is populated from the decoded JWT passed via the API token.
// Currently a passthrough because access control is handled in the Next.js API layer
// before reaching Cube. When Cube is exposed more broadly or multi-tenancy enforcement
// is needed at the Cube level, add filters here based on securityContext claims.
queryRewrite: (query, { securityContext }) => {
return query;
},
};

View File

@@ -0,0 +1,159 @@
// This schema maps to the `feedback_records` table owned by the Formbricks Hub Postgres.
// If the Hub changes column names, types, or the metadata JSONB shape (e.g. the `topics` array),
// this schema must be updated to match.
cube(`FeedbackRecords`, {
sql: `SELECT * FROM feedback_records`,
measures: {
count: {
type: `count`,
description: `Total number of feedback responses`,
},
promoterCount: {
type: `count`,
filters: [{ sql: `${CUBE}.value_number >= 9` }],
description: `Number of promoters (NPS score 9-10)`,
},
detractorCount: {
type: `count`,
filters: [{ sql: `${CUBE}.value_number <= 6` }],
description: `Number of detractors (NPS score 0-6)`,
},
passiveCount: {
type: `count`,
filters: [{ sql: `${CUBE}.value_number >= 7 AND ${CUBE}.value_number <= 8` }],
description: `Number of passives (NPS score 7-8)`,
},
npsScore: {
type: `number`,
sql: `
CASE
WHEN COUNT(*) = 0 THEN 0
ELSE ROUND(
(
(COUNT(CASE WHEN ${CUBE}.value_number >= 9 THEN 1 END)::numeric -
COUNT(CASE WHEN ${CUBE}.value_number <= 6 THEN 1 END)::numeric)
/ COUNT(*)::numeric
) * 100,
2
)
END
`,
description: `Net Promoter Score: ((Promoters - Detractors) / Total) * 100`,
},
averageScore: {
type: `avg`,
sql: `${CUBE}.value_number`,
description: `Average NPS score`,
},
},
dimensions: {
id: {
sql: `id`,
type: `string`,
primaryKey: true,
},
sentiment: {
sql: `sentiment`,
type: `string`,
description: `Sentiment extracted from metadata JSONB field`,
},
sourceType: {
sql: `source_type`,
type: `string`,
description: `Source type of the feedback (e.g., nps_campaign, survey)`,
},
sourceName: {
sql: `source_name`,
type: `string`,
description: `Human-readable name of the source`,
},
fieldType: {
sql: `field_type`,
type: `string`,
description: `Type of feedback field (e.g., nps, text, rating)`,
},
collectedAt: {
sql: `collected_at`,
type: `time`,
description: `Timestamp when the feedback was collected`,
},
npsValue: {
sql: `value_number`,
type: `number`,
description: `Raw NPS score value (0-10)`,
},
responseId: {
sql: `response_id`,
type: `string`,
description: `Unique identifier linking related feedback records`,
},
userIdentifier: {
sql: `user_identifier`,
type: `string`,
description: `Identifier of the user who provided feedback`,
},
emotion: {
sql: `emotion`,
type: `string`,
description: `Emotion extracted from metadata JSONB field`,
},
},
joins: {
TopicsUnnested: {
sql: `${CUBE}.id = ${TopicsUnnested}.feedback_record_id`,
relationship: `hasMany`,
},
},
});
cube(`TopicsUnnested`, {
sql: `
SELECT
fr.id as feedback_record_id,
topic_elem.topic
FROM feedback_records fr
CROSS JOIN LATERAL jsonb_array_elements_text(COALESCE(fr.metadata->'topics', '[]'::jsonb)) AS topic_elem(topic)
`,
measures: {
count: {
type: `count`,
},
},
dimensions: {
id: {
sql: `feedback_record_id || '-' || topic`,
type: `string`,
primaryKey: true,
},
feedbackRecordId: {
sql: `feedback_record_id`,
type: `string`,
},
topic: {
sql: `topic`,
type: `string`,
description: `Individual topic from the topics array`,
},
},
});

View File

@@ -36,6 +36,34 @@ services:
volumes:
- minio-data:/data
# Cube connects to the Formbricks Hub Postgres which owns the feedback_records table.
# The CUBEJS_DB_* defaults below must match the Hub's Postgres credentials.
# Override via env vars if your Hub database uses different credentials or runs on another host.
#
# SECURITY: CUBEJS_API_SECRET has no default and must be set explicitly (e.g. in .env).
# Never use a weak secret in production/staging. Generate with: openssl rand -hex 32
cube:
image: cubejs/cube:v1.6.6
ports:
- 4000:4000
- 4001:4001 # Cube Playground UI (dev only)
environment:
CUBEJS_DB_TYPE: postgres
CUBEJS_DB_HOST: ${CUBEJS_DB_HOST:-postgres}
CUBEJS_DB_NAME: ${CUBEJS_DB_NAME:-postgres}
CUBEJS_DB_USER: ${CUBEJS_DB_USER:-postgres}
CUBEJS_DB_PASS: ${CUBEJS_DB_PASS:-postgres}
CUBEJS_DB_PORT: ${CUBEJS_DB_PORT:-5432}
CUBEJS_DEV_MODE: "true"
CUBEJS_API_SECRET: ${CUBEJS_API_SECRET}
CUBEJS_CACHE_AND_QUEUE_DRIVER: memory
volumes:
- ./cube/cube.js:/cube/conf/cube.js
- ./cube/schema:/cube/conf/model
restart: on-failure
depends_on:
- postgres
volumes:
postgres:
driver: local

View File

@@ -2,6 +2,7 @@
/* eslint-disable @typescript-eslint/no-namespace -- using namespaces is required for prisma-json-types-generator */
import { type TActionClassNoCodeConfig } from "../types/action-classes";
import type { TOrganizationAccess } from "../types/api-key";
import type { TChartConfig, TChartQuery, TWidgetLayout } from "../types/dashboard";
import { type TIntegrationConfig } from "../types/integration";
import { type TOrganizationBilling } from "../types/organizations";
import { type TProjectConfig, type TProjectStyling } from "../types/project";
@@ -55,5 +56,8 @@ declare global {
export type OrganizationAccess = TOrganizationAccess;
export type SurveyMetadata = TSurveyMetadata;
export type SurveyQuotaLogic = TSurveyQuotaLogic;
export type ChartQuery = TChartQuery;
export type ChartConfig = TChartConfig;
export type WidgetLayout = TWidgetLayout;
}
}

View File

@@ -0,0 +1,77 @@
-- CreateEnum
CREATE TYPE "public"."ChartType" AS ENUM ('area', 'bar', 'line', 'pie', 'big_number');
-- CreateTable
CREATE TABLE "public"."Chart" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"name" TEXT NOT NULL,
"type" "public"."ChartType" NOT NULL,
"projectId" TEXT NOT NULL,
"query" JSONB NOT NULL DEFAULT '{}',
"config" JSONB NOT NULL DEFAULT '{}',
"createdBy" TEXT,
CONSTRAINT "Chart_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."Dashboard" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"projectId" TEXT NOT NULL,
"createdBy" TEXT,
CONSTRAINT "Dashboard_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."DashboardWidget" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"dashboardId" TEXT NOT NULL,
"title" TEXT,
"chartId" TEXT NOT NULL,
"layout" JSONB NOT NULL DEFAULT '{"x":0,"y":0,"w":4,"h":3}',
"order" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "DashboardWidget_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Chart_projectId_created_at_idx" ON "public"."Chart"("projectId", "created_at");
-- CreateIndex
CREATE UNIQUE INDEX "Chart_projectId_name_key" ON "public"."Chart"("projectId", "name");
-- CreateIndex
CREATE INDEX "Dashboard_projectId_created_at_idx" ON "public"."Dashboard"("projectId", "created_at");
-- CreateIndex
CREATE UNIQUE INDEX "Dashboard_projectId_name_key" ON "public"."Dashboard"("projectId", "name");
-- CreateIndex
CREATE INDEX "DashboardWidget_dashboardId_order_idx" ON "public"."DashboardWidget"("dashboardId", "order");
-- AddForeignKey
ALTER TABLE "public"."Chart" ADD CONSTRAINT "Chart_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "public"."Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."Chart" ADD CONSTRAINT "Chart_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "public"."User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."Dashboard" ADD CONSTRAINT "Dashboard_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "public"."Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."Dashboard" ADD CONSTRAINT "Dashboard_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "public"."User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."DashboardWidget" ADD CONSTRAINT "DashboardWidget_dashboardId_fkey" FOREIGN KEY ("dashboardId") REFERENCES "public"."Dashboard"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."DashboardWidget" ADD CONSTRAINT "DashboardWidget_chartId_fkey" FOREIGN KEY ("chartId") REFERENCES "public"."Chart"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -647,6 +647,8 @@ model Project {
logo Json?
projectTeams ProjectTeam[]
customHeadScripts String? // Custom HTML scripts for link surveys (self-hosted only)
charts Chart[]
dashboards Dashboard[]
@@unique([organizationId, name])
}
@@ -867,6 +869,8 @@ model User {
/// [Locale]
locale String @default("en-US")
surveys Survey[]
charts Chart[] @relation("chartCreatedBy")
dashboards Dashboard[] @relation("dashboardCreatedBy")
teamUsers TeamUser[]
lastLoginAt DateTime?
isActive Boolean @default(true)
@@ -1004,3 +1008,92 @@ model ProjectTeam {
@@id([projectId, teamId])
@@index([teamId])
}
enum ChartType {
area
bar
line
pie
big_number
}
/// Represents a chart/visualization that can be used in multiple dashboards.
/// Charts are reusable components that query analytics data.
///
/// @property id - Unique identifier for the chart
/// @property name - Display name of the chart
/// @property type - Type of visualization (bar, line, pie, etc.)
/// @property project - The project this chart belongs to
/// @property query - Cube.js query configuration (JSON)
/// @property config - Chart-specific configuration (colors, labels, etc.)
/// @property createdBy - User who created the chart
/// @property dashboards - Dashboards that use this chart
model Chart {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
name String
type ChartType
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId String
/// [ChartQuery] - Cube.js query configuration
query Json @default("{}")
/// [ChartConfig] - Visualization configuration (colors, labels, formatting)
config Json @default("{}")
creator User? @relation("chartCreatedBy", fields: [createdBy], references: [id], onDelete: SetNull)
createdBy String?
widgets DashboardWidget[]
@@unique([projectId, name])
@@index([projectId, createdAt])
}
/// Represents a dashboard containing multiple charts.
/// Dashboards aggregate analytics insights at the project level.
///
/// @property id - Unique identifier for the dashboard
/// @property name - Display name of the dashboard
/// @property description - Optional description
/// @property project - The project this dashboard belongs to
/// @property widgets - Charts on this dashboard
/// @property createdBy - User who created the dashboard
model Dashboard {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
name String
description String?
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId String
creator User? @relation("dashboardCreatedBy", fields: [createdBy], references: [id], onDelete: SetNull)
createdBy String?
widgets DashboardWidget[]
@@unique([projectId, name])
@@index([projectId, createdAt])
}
/// Represents a chart widget on a dashboard.
/// Widgets are positioned using a grid layout system.
///
/// @property id - Unique identifier for the widget
/// @property dashboard - The dashboard this widget belongs to
/// @property title - Optional title for the widget
/// @property chart - The chart displayed in this widget
/// @property layout - Grid layout configuration (x, y, width, height)
/// @property order - Display order within the dashboard
model DashboardWidget {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
dashboard Dashboard @relation(fields: [dashboardId], references: [id], onDelete: Cascade)
dashboardId String
title String?
chart Chart @relation(fields: [chartId], references: [id], onDelete: Cascade)
chartId String
/// [WidgetLayout] - Grid layout: { x, y, w, h }
layout Json @default("{\"x\":0,\"y\":0,\"w\":4,\"h\":3}")
order Int @default(0)
@@index([dashboardId, order])
}

View File

@@ -0,0 +1,75 @@
import { z } from "zod";
// ── Cube.js Query shape (stored as JSON in Chart.query) ─────────────────────
// Mirrors https://cube.dev/docs/product/apis-integrations/core-data-apis/rest-api/query-format
const ZCubeTimeDimension = z.object({
dimension: z.string(),
dateRange: z.union([z.string(), z.tuple([z.string(), z.string()])]).optional(),
compareDateRange: z.array(z.union([z.string(), z.tuple([z.string(), z.string()])])).optional(),
granularity: z.enum(["second", "minute", "hour", "day", "week", "month", "quarter", "year"]).optional(),
});
const ZCubeMemberFilter = z.object({
member: z.string(),
operator: z.string(),
values: z.array(z.string()).optional(),
});
type TCubeFilter = z.infer<typeof ZCubeMemberFilter> | { and: TCubeFilter[] } | { or: TCubeFilter[] };
const ZCubeFilter: z.ZodType<TCubeFilter> = z.union([
ZCubeMemberFilter,
z.object({ and: z.lazy(() => z.array(ZCubeFilter)) }),
z.object({ or: z.lazy(() => z.array(ZCubeFilter)) }),
]);
export const ZChartQuery = z.object({
measures: z.array(z.string()).optional(),
dimensions: z.array(z.string()).optional(),
segments: z.array(z.string()).optional(),
timeDimensions: z.array(ZCubeTimeDimension).optional(),
filters: z.array(ZCubeFilter).optional(),
order: z
.union([z.array(z.tuple([z.string(), z.enum(["asc", "desc"])])), z.record(z.enum(["asc", "desc"]))])
.optional(),
limit: z.number().int().positive().optional(),
total: z.boolean().optional(),
offset: z.number().int().nonnegative().optional(),
timezone: z.string().optional(),
renewQuery: z.boolean().optional(),
ungrouped: z.boolean().optional(),
joinHints: z.array(z.tuple([z.string(), z.string()])).optional(),
});
export type TChartQuery = z.infer<typeof ZChartQuery>;
// ── Chart visualization config (stored as JSON in Chart.config) ─────────────
export const ZChartConfig = z.object({
colors: z.array(z.string()).optional(),
xAxisLabel: z.string().optional(),
yAxisLabel: z.string().optional(),
showLegend: z.boolean().optional(),
legendPosition: z.enum(["top", "bottom", "left", "right"]).optional(),
stacked: z.boolean().optional(),
showGrid: z.boolean().optional(),
showValues: z.boolean().optional(),
numberFormat: z.string().optional(),
dateFormat: z.string().optional(),
prefix: z.string().optional(),
suffix: z.string().optional(),
});
export type TChartConfig = z.infer<typeof ZChartConfig>;
// ── Widget grid layout (stored as JSON in DashboardWidget.layout) ───────────
export const ZWidgetLayout = z.object({
x: z.number().int().nonnegative(),
y: z.number().int().nonnegative(),
w: z.number().int().positive(),
h: z.number().int().positive(),
});
export type TWidgetLayout = z.infer<typeof ZWidgetLayout>;

73
pnpm-lock.yaml generated
View File

@@ -28,7 +28,7 @@ importers:
dependencies:
next:
specifier: 16.1.6
version: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react:
specifier: 19.2.3
version: 19.2.3
@@ -139,6 +139,9 @@ importers:
'@boxyhq/saml-jackson':
specifier: 1.52.2
version: 1.52.2(socks@2.8.7)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.8.3))
'@cubejs-client/core':
specifier: 1.6.6
version: 1.6.6(encoding@0.1.13)
'@dnd-kit/core':
specifier: 6.3.1
version: 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -294,7 +297,7 @@ importers:
version: 1.2.6(@types/react-dom@19.2.1(@types/react@19.2.1))(@types/react@19.2.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@sentry/nextjs':
specifier: 10.5.0
version: 10.5.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.99.8(esbuild@0.25.12))
version: 10.5.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.99.8(esbuild@0.25.12))
'@t3-oss/env-nextjs':
specifier: 0.13.4
version: 0.13.4(arktype@2.1.29)(typescript@5.8.3)(zod@3.24.4)
@@ -384,13 +387,13 @@ importers:
version: 3.0.1
next:
specifier: 16.1.6
version: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next-auth:
specifier: 4.24.12
version: 4.24.12(patch_hash=7ac5717a8d7d2049442182b5d83ab492d33fe774ff51ff5ea3884628b77df87b)(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(nodemailer@7.0.11)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
version: 4.24.12(patch_hash=7ac5717a8d7d2049442182b5d83ab492d33fe774ff51ff5ea3884628b77df87b)(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(nodemailer@7.0.11)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next-safe-action:
specifier: 7.10.8
version: 7.10.8(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.24.4)
version: 7.10.8(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.24.4)
node-fetch:
specifier: 3.3.2
version: 3.3.2
@@ -1775,6 +1778,9 @@ packages:
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
engines: {node: '>=18'}
'@cubejs-client/core@1.6.6':
resolution: {integrity: sha512-JWEVQaaS7y6Y3Nhe4Lcjl8coP5eXIgYZQTb2WMlLdMluSGM4mWOSFpAis77lAPV+nybFvRf9ri+PjCGqGJXH5g==}
'@date-fns/tz@1.2.0':
resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==}
@@ -7155,6 +7161,9 @@ packages:
core-js-compat@3.47.0:
resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==}
core-js@3.48.0:
resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
@@ -7170,6 +7179,9 @@ packages:
engines: {node: '>=20'}
hasBin: true
cross-fetch@3.2.0:
resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -10035,6 +10047,9 @@ packages:
quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
ramda@0.27.2:
resolution: {integrity: sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==}
randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
@@ -11306,6 +11321,9 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
url-search-params-polyfill@7.0.1:
resolution: {integrity: sha512-bAw7L2E+jn9XHG5P9zrPnHdO0yJub4U+yXJOdpcpkr7OBd9T8oll4lUos0iSGRcDvfZoLUKfx9a6aNmIhJ4+mQ==}
url-template@2.0.8:
resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==}
@@ -13515,6 +13533,17 @@ snapshots:
'@csstools/css-tokenizer@3.0.4': {}
'@cubejs-client/core@1.6.6(encoding@0.1.13)':
dependencies:
core-js: 3.48.0
cross-fetch: 3.2.0(encoding@0.1.13)
dayjs: 1.11.19
ramda: 0.27.2
url-search-params-polyfill: 7.0.1
uuid: 11.1.0
transitivePeerDependencies:
- encoding
'@date-fns/tz@1.2.0': {}
'@dnd-kit/accessibility@3.1.1(react@19.2.3)':
@@ -17049,7 +17078,7 @@ snapshots:
'@sentry/core@10.5.0': {}
'@sentry/nextjs@10.5.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.99.8(esbuild@0.25.12))':
'@sentry/nextjs@10.5.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.99.8(esbuild@0.25.12))':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/semantic-conventions': 1.38.0
@@ -17062,7 +17091,7 @@ snapshots:
'@sentry/vercel-edge': 10.5.0
'@sentry/webpack-plugin': 4.6.1(encoding@0.1.13)(webpack@5.99.8(esbuild@0.25.12))
chalk: 3.0.0
next: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
resolve: 1.22.8
rollup: 4.54.0
stacktrace-parser: 0.1.11
@@ -19786,6 +19815,8 @@ snapshots:
dependencies:
browserslist: 4.28.1
core-js@3.48.0: {}
core-util-is@1.0.3: {}
cors@2.8.5:
@@ -19800,6 +19831,12 @@ snapshots:
'@epic-web/invariant': 1.0.0
cross-spawn: 7.0.6
cross-fetch@3.2.0(encoding@0.1.13):
dependencies:
node-fetch: 2.7.0(encoding@0.1.13)
transitivePeerDependencies:
- encoding
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -22186,13 +22223,13 @@ snapshots:
neo-async@2.6.2: {}
next-auth@4.24.12(patch_hash=7ac5717a8d7d2049442182b5d83ab492d33fe774ff51ff5ea3884628b77df87b)(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(nodemailer@7.0.11)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
next-auth@4.24.12(patch_hash=7ac5717a8d7d2049442182b5d83ab492d33fe774ff51ff5ea3884628b77df87b)(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(nodemailer@7.0.11)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
'@babel/runtime': 7.28.4
'@panva/hkdf': 1.2.1
cookie: 0.7.2
jose: 4.15.9
next: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
oauth: 0.9.15
openid-client: 5.7.1
preact: 10.28.2
@@ -22203,9 +22240,9 @@ snapshots:
optionalDependencies:
nodemailer: 7.0.11
next-safe-action@7.10.8(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.24.4):
next-safe-action@7.10.8(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.24.4):
dependencies:
next: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
optionalDependencies:
@@ -22219,7 +22256,7 @@ snapshots:
postcss: 8.4.31
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3)
styled-jsx: 5.1.6(react@19.2.3)
optionalDependencies:
'@next/swc-darwin-arm64': 16.0.10
'@next/swc-darwin-x64': 16.0.10
@@ -22236,7 +22273,7 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
'@next/env': 16.1.6
'@swc/helpers': 0.5.15
@@ -22245,7 +22282,7 @@ snapshots:
postcss: 8.4.31
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3)
styled-jsx: 5.1.6(react@19.2.3)
optionalDependencies:
'@next/swc-darwin-arm64': 16.1.6
'@next/swc-darwin-x64': 16.1.6
@@ -22933,6 +22970,8 @@ snapshots:
quick-format-unescaped@4.0.4: {}
ramda@0.27.2: {}
randombytes@2.1.0:
dependencies:
safe-buffer: 5.2.1
@@ -23991,12 +24030,10 @@ snapshots:
stubborn-utils@1.0.2: {}
styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.3):
styled-jsx@5.1.6(react@19.2.3):
dependencies:
client-only: 0.0.1
react: 19.2.3
optionalDependencies:
'@babel/core': 7.28.5
stylis@4.3.6: {}
@@ -24504,6 +24541,8 @@ snapshots:
dependencies:
punycode: 2.3.1
url-search-params-polyfill@7.0.1: {}
url-template@2.0.8: {}
use-callback-ref@1.3.3(@types/react@19.2.1)(react@19.2.1):

View File

@@ -156,6 +156,8 @@
"BREVO_API_KEY",
"BREVO_LIST_ID",
"CRON_SECRET",
"CUBEJS_API_TOKEN",
"CUBEJS_API_URL",
"DATABASE_URL",
"DEBUG",
"E2E_TESTING",