mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-19 00:51:06 -06:00
Compare commits
8 Commits
fix/dataty
...
epic/dashb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f49f40610b | ||
|
|
9e754bad9c | ||
|
|
4dcf6fda40 | ||
|
|
1b8ccd7199 | ||
|
|
4f9088559f | ||
|
|
18550f1d11 | ||
|
|
881cd31f74 | ||
|
|
e00405dca2 |
@@ -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
|
||||
29
apps/web/modules/ee/analytics/api/lib/cube-client.test.ts
Normal file
29
apps/web/modules/ee/analytics/api/lib/cube-client.test.ts
Normal 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 }]);
|
||||
});
|
||||
});
|
||||
25
apps/web/modules/ee/analytics/api/lib/cube-client.ts
Normal file
25
apps/web/modules/ee/analytics/api/lib/cube-client.ts
Normal 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();
|
||||
}
|
||||
@@ -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
12
cube/cube.js
Normal 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;
|
||||
},
|
||||
};
|
||||
159
cube/schema/FeedbackRecords.js
Normal file
159
cube/schema/FeedbackRecords.js
Normal 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`,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
75
packages/types/dashboard.ts
Normal file
75
packages/types/dashboard.ts
Normal 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
73
pnpm-lock.yaml
generated
@@ -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):
|
||||
|
||||
@@ -156,6 +156,8 @@
|
||||
"BREVO_API_KEY",
|
||||
"BREVO_LIST_ID",
|
||||
"CRON_SECRET",
|
||||
"CUBEJS_API_TOKEN",
|
||||
"CUBEJS_API_URL",
|
||||
"DATABASE_URL",
|
||||
"DEBUG",
|
||||
"E2E_TESTING",
|
||||
|
||||
Reference in New Issue
Block a user