feat: dashboard v2, esm, upgrades (#211)

* esm

* wip

* wip

* wip

* wip

* wip

* wip

* subscription notice

* wip

* wip

* wip

* fix envs

* fix: update docker build

* fix

* esm/types

* delete dashboard :D

* add patches to dockerfiles

* update packages + catalogs + ts

* wip

* remove native libs

* ts

* improvements

* fix redirects and fetching session

* try fix favicon

* fixes

* fix

* order and resize reportds within a dashboard

* improvements

* wip

* added userjot to dashboard

* fix

* add op

* wip

* different cache key

* improve date picker

* fix table

* event details loading

* redo onboarding completely

* fix login

* fix

* fix

* extend session, billing and improve bars

* fix

* reduce price on 10M
This commit is contained in:
Carl-Gerhard Lindesvärd
2025-10-16 12:27:44 +02:00
committed by GitHub
parent 436e81ecc9
commit 81a7e5d62e
741 changed files with 32695 additions and 16996 deletions

View File

@@ -0,0 +1,29 @@
-- AlterTable
ALTER TABLE "public"."_IntegrationToNotificationRule" ADD CONSTRAINT "_IntegrationToNotificationRule_AB_pkey" PRIMARY KEY ("A", "B");
-- DropIndex
DROP INDEX "public"."_IntegrationToNotificationRule_AB_unique";
-- CreateTable
CREATE TABLE "public"."report_layouts" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"reportId" UUID NOT NULL,
"x" INTEGER NOT NULL DEFAULT 0,
"y" INTEGER NOT NULL DEFAULT 0,
"w" INTEGER NOT NULL DEFAULT 4,
"h" INTEGER NOT NULL DEFAULT 3,
"minW" INTEGER DEFAULT 2,
"minH" INTEGER DEFAULT 2,
"maxW" INTEGER,
"maxH" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "report_layouts_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "report_layouts_reportId_key" ON "public"."report_layouts"("reportId");
-- AddForeignKey
ALTER TABLE "public"."report_layouts" ADD CONSTRAINT "report_layouts_reportId_fkey" FOREIGN KEY ("reportId") REFERENCES "public"."reports"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

@@ -0,0 +1,210 @@
import { readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
interface JsonFieldMapping {
model: string;
field: string;
type: string;
}
function parseSchemaForJsonTypes(schemaPath: string): JsonFieldMapping[] {
const schemaContent = readFileSync(schemaPath, 'utf-8');
const lines = schemaContent.split('\n');
const mappings: JsonFieldMapping[] = [];
let currentModel = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i]?.trim() || '';
// Track current model
if (line.startsWith('model ')) {
const parts = line.split(' ');
currentModel = parts[1] || '';
continue;
}
// Look for Json fields with type comments
if (line.includes('Json') && i > 0) {
const prevLine = lines[i - 1]?.trim() || '';
const typeMatch = prevLine.match(/\/\/\/ \[([^\]]+)\]/);
if (typeMatch) {
const fieldMatch = line.match(/(\w+)\s+Json/);
if (fieldMatch?.[1] && typeMatch[1]) {
mappings.push({
model: currentModel,
field: fieldMatch[1],
type: typeMatch[1],
});
}
}
}
}
return mappings;
}
function processGeneratedFiles(
generatedDir: string,
mappings: JsonFieldMapping[],
): void {
// Process the main files in the generated directory
const mainFiles = [
'client.ts',
'commonInputTypes.ts',
'enums.ts',
'models.ts',
];
for (const fileName of mainFiles) {
const filePath = join(generatedDir, fileName);
try {
replaceJsonValueInFileForModel(filePath, mappings);
} catch (error) {
console.log(`Skipping ${filePath}: ${error}`);
}
}
// Process files in the models subdirectory - each file corresponds to one model
const modelsDir = join(generatedDir, 'models');
try {
const modelFiles = readdirSync(modelsDir);
for (const fileName of modelFiles) {
if (fileName.endsWith('.ts')) {
const filePath = join(modelsDir, fileName);
try {
// Extract model name from filename (e.g., "Notification.ts" -> "Notification")
const modelName = fileName.replace('.ts', '');
// Only process mappings for this specific model
const modelMappings = mappings.filter((m) => m.model === modelName);
if (modelMappings.length > 0) {
replaceJsonValueInFileForModel(filePath, modelMappings);
}
} catch (error) {
console.log(`Skipping ${filePath}: ${error}`);
}
}
}
} catch (error) {
console.log(`Could not read models directory: ${error}`);
}
}
function replaceJsonValueInFileForModel(
filePath: string,
mappings: JsonFieldMapping[],
): void {
let content = readFileSync(filePath, 'utf-8');
let modified = false;
for (const mapping of mappings) {
// Pattern 1: Simple runtime.JsonValue replacement (for select/return types)
const simpleJsonValueRegex = new RegExp(
`\\b${mapping.field}:\\s*runtime\\.JsonValue\\b`,
'g',
);
if (simpleJsonValueRegex.test(content)) {
content = content.replace(
simpleJsonValueRegex,
`${mapping.field}: PrismaJson.${mapping.type}`,
);
modified = true;
}
// Pattern 2: runtime.InputJsonValue with optional JsonNullValueInput (for create/update inputs)
const inputJsonValueRegex = new RegExp(
`\\b${mapping.field}:\\s*(?:Prisma\\.JsonNullValueInput\\s*\\|\\s*)?runtime\\.InputJsonValue\\b`,
'g',
);
if (inputJsonValueRegex.test(content)) {
content = content.replace(
inputJsonValueRegex,
`${mapping.field}: PrismaJson.${mapping.type}`,
);
modified = true;
}
// Pattern 3: Optional runtime.InputJsonValue with optional JsonNullValueInput
const optionalInputJsonValueRegex = new RegExp(
`\\b${mapping.field}\\?:\\s*(?:Prisma\\.JsonNullValueInput\\s*\\|\\s*)?runtime\\.InputJsonValue\\b`,
'g',
);
if (optionalInputJsonValueRegex.test(content)) {
content = content.replace(
optionalInputJsonValueRegex,
`${mapping.field}?: PrismaJson.${mapping.type}`,
);
modified = true;
}
// Pattern 4: Union types with JsonNullValueInput | runtime.InputJsonValue
const unionJsonValueRegex =
/(Prisma\.JsonNullValueInput\s*\|\s*)runtime\.InputJsonValue/g;
if (unionJsonValueRegex.test(content)) {
content = content.replace(
unionJsonValueRegex,
`$1PrismaJson.${mapping.type}`,
);
modified = true;
}
// Pattern 5: Just runtime.InputJsonValue in unions
const simpleInputJsonValueRegex = /\|\s*runtime\.InputJsonValue/g;
if (simpleInputJsonValueRegex.test(content)) {
content = content.replace(
simpleInputJsonValueRegex,
`| PrismaJson.${mapping.type}`,
);
modified = true;
}
// Pattern 6: Optional union types with JsonNullValueInput | runtime.InputJsonValue
const optionalUnionJsonValueRegex = new RegExp(
`\\b${mapping.field}\\?:\\s*(?:Prisma\\.JsonNullValueInput\\s*\\|\\s*)?runtime\\.InputJsonValue\\b`,
'g',
);
if (optionalUnionJsonValueRegex.test(content)) {
content = content.replace(
optionalUnionJsonValueRegex,
`${mapping.field}?: PrismaJson.${mapping.type}`,
);
modified = true;
}
}
if (modified) {
writeFileSync(filePath, content, 'utf-8');
console.log(`Updated ${filePath}`);
}
}
function main() {
const schemaPath = join(__dirname, '../prisma/schema.prisma');
const generatedDir = join(__dirname, '../src/generated/prisma');
console.log('Parsing schema for Json type mappings...');
const mappings = parseSchemaForJsonTypes(schemaPath);
console.log('Found Json type mappings:');
mappings.forEach((m) => console.log(` ${m.model}.${m.field} -> ${m.type}`));
if (mappings.length === 0) {
console.log('No mappings found!');
return;
}
console.log('Processing generated files...');
processGeneratedFiles(generatedDir, mappings);
console.log('Post-codegen script completed!');
}
main();

View File

@@ -2,12 +2,16 @@
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
provider = "prisma-client"
output = "../src/generated/prisma"
moduleFormat = "esm"
generatedFileExtension = "ts"
importFileExtension = "ts"
}
generator json {
provider = "prisma-json-types-generator"
}
// generator json {
// provider = "prisma-json-types-generator"
// }
datasource db {
provider = "postgresql"
@@ -315,7 +319,8 @@ model Report {
funnelWindow Float?
dashboardId String
dashboard Dashboard @relation(fields: [dashboardId], references: [id], onDelete: Cascade)
dashboard Dashboard @relation(fields: [dashboardId], references: [id], onDelete: Cascade)
layout ReportLayout?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@ -323,6 +328,29 @@ model Report {
@@map("reports")
}
model ReportLayout {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
reportId String @unique @db.Uuid
report Report @relation(fields: [reportId], references: [id], onDelete: Cascade)
// Grid position and size
x Int @default(0)
y Int @default(0)
w Int @default(4)
h Int @default(3)
// Optional: store additional layout preferences
minW Int? @default(2)
minH Int? @default(2)
maxW Int?
maxH Int?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@map("report_layouts")
}
model ShareOverview {
id String @unique
projectId String @unique