wip: docker

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-01-14 07:39:02 +01:00
parent 1b10371940
commit 719a82f1c4
68 changed files with 3105 additions and 328 deletions

View File

@@ -1,25 +0,0 @@
# Since the ".env" file is gitignored, you can use the ".env.example" file to
# build a new ".env" file when you clone the repo. Keep this file up-to-date
# when you add new variables to `.env`.
# This file will be committed to version control, so make sure not to have any
# secrets in it. If you are cloning this repo, create a copy of this file named
# ".env" and populate it with your secrets.
# When adding additional environment variables, the schema in "/src/env.mjs"
# should be updated accordingly.
# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
DATABASE_URL="file:./db.sqlite"
# Next Auth
# You can generate a new secret on the command line with:
# openssl rand -base64 32
# https://next-auth.js.org/configuration/options#secret
# NEXTAUTH_SECRET=""
NEXTAUTH_URL="http://localhost:3000"
# Next Auth Discord Provider
DISCORD_CLIENT_ID=""
DISCORD_CLIENT_SECRET=""

View File

@@ -7,9 +7,13 @@ await import('./src/env.mjs');
/** @type {import("next").NextConfig} */
const config = {
reactStrictMode: true,
transpilePackages: [],
transpilePackages: ['@mixan/queue'],
eslint: { ignoreDuringBuilds: true },
typescript: { ignoreBuildErrors: true },
experimental: {
// Avoid "Critical dependency: the request of a dependency is an expression"
serverComponentsExternalPackages: ['bullmq'],
},
/**
* If you are using `appDir` then you must comment the below `i18n` config out.
*

View File

@@ -3,19 +3,19 @@
"version": "0.1.0",
"private": true,
"scripts": {
"postinstall": "prisma generate",
"dev": "next dev",
"dev": "pnpm with-env next dev",
"build": "next build",
"start": "next start",
"lint": "eslint .",
"format": "prettier --write \"**/*.{tsx,mjs,ts,md,json}\"",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env -c --"
},
"dependencies": {
"@hookform/resolvers": "^3.3.2",
"@mixan/db": "workspace:^",
"@mixan/queue": "workspace:^",
"@mixan/types": "workspace:*",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^5.1.1",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-aspect-ratio": "^1.0.3",
"@radix-ui/react-avatar": "^1.0.4",
@@ -83,7 +83,6 @@
"postcss": "^8.4.27",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.1",
"prisma": "^5.1.1",
"tailwindcss": "^3.3.3",
"typescript": "^5.2.2"
},

View File

@@ -1,59 +0,0 @@
-- CreateTable
CREATE TABLE "organizations" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"name" TEXT NOT NULL,
CONSTRAINT "organizations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "projects" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"name" TEXT NOT NULL,
"organization_id" UUID NOT NULL,
CONSTRAINT "projects_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "users" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"name" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"organization_id" UUID NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "events" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"name" TEXT NOT NULL,
"properties" JSONB NOT NULL,
"project_id" UUID NOT NULL,
CONSTRAINT "events_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "profiles" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"name" TEXT NOT NULL,
"properties" JSONB NOT NULL,
"project_id" UUID NOT NULL,
CONSTRAINT "profiles_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "projects" ADD CONSTRAINT "projects_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "users" ADD CONSTRAINT "users_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "events" ADD CONSTRAINT "events_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "profiles" ADD CONSTRAINT "profiles_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,19 +0,0 @@
-- AlterTable
ALTER TABLE "events" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "organizations" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "profiles" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "projects" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "users" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@@ -1,10 +0,0 @@
/*
Warnings:
- You are about to drop the column `name` on the `profiles` table. All the data in the column will be lost.
- Added the required column `profile_id` to the `profiles` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "profiles" DROP COLUMN "name",
ADD COLUMN "profile_id" TEXT NOT NULL;

View File

@@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE "profiles" ADD COLUMN "avatar" TEXT,
ADD COLUMN "email" TEXT,
ADD COLUMN "first_name" TEXT,
ADD COLUMN "last_name" TEXT;

View File

@@ -1,12 +0,0 @@
-- CreateTable
CREATE TABLE "clients" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"name" TEXT NOT NULL,
"secret" TEXT NOT NULL,
"project_id" UUID NOT NULL,
CONSTRAINT "clients_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "clients" ADD CONSTRAINT "clients_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "clients" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@@ -1,11 +0,0 @@
/*
Warnings:
- Added the required column `profile_id` to the `events` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "events" ADD COLUMN "profile_id" UUID NOT NULL;
-- AddForeignKey
ALTER TABLE "events" ADD CONSTRAINT "events_profile_id_fkey" FOREIGN KEY ("profile_id") REFERENCES "profiles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,8 +0,0 @@
-- DropForeignKey
ALTER TABLE "events" DROP CONSTRAINT "events_profile_id_fkey";
-- AlterTable
ALTER TABLE "events" ALTER COLUMN "profile_id" DROP NOT NULL;
-- AddForeignKey
ALTER TABLE "events" ADD CONSTRAINT "events_profile_id_fkey" FOREIGN KEY ("profile_id") REFERENCES "profiles"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1,10 +0,0 @@
/*
Warnings:
- You are about to drop the column `profile_id` on the `profiles` table. All the data in the column will be lost.
- Added the required column `external_id` to the `profiles` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "profiles" DROP COLUMN "profile_id",
ADD COLUMN "external_id" TEXT NOT NULL;

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[project_id,external_id]` on the table `profiles` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "profiles_project_id_external_id_key" ON "profiles"("project_id", "external_id");

View File

@@ -1,5 +0,0 @@
-- DropIndex
DROP INDEX "profiles_project_id_external_id_key";
-- AlterTable
ALTER TABLE "profiles" ALTER COLUMN "external_id" DROP NOT NULL;

View File

@@ -1,41 +0,0 @@
-- CreateEnum
CREATE TYPE "Interval" AS ENUM ('hour', 'day', 'month');
-- CreateEnum
CREATE TYPE "ChartType" AS ENUM ('linear', 'bar', 'pie', 'metric', 'area');
-- CreateTable
CREATE TABLE "dashboards" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"name" TEXT NOT NULL,
"project_id" UUID NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "dashboards_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "reports" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"interval" "Interval" NOT NULL,
"range" INTEGER NOT NULL,
"chart_type" "ChartType" NOT NULL,
"breakdowns" JSONB NOT NULL,
"events" JSONB NOT NULL,
"project_id" UUID NOT NULL,
"dashboard_id" UUID NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "reports_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "dashboards" ADD CONSTRAINT "dashboards_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "reports" ADD CONSTRAINT "reports_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "reports" ADD CONSTRAINT "reports_dashboard_id_fkey" FOREIGN KEY ("dashboard_id") REFERENCES "dashboards"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `name` to the `reports` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "reports" ADD COLUMN "name" TEXT NOT NULL;

View File

@@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE "clients" ADD COLUMN "organization_id" UUID;
-- AddForeignKey
ALTER TABLE "clients" ADD CONSTRAINT "clients_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1,14 +0,0 @@
/*
Warnings:
- Made the column `organization_id` on table `clients` required. This step will fail if there are existing NULL values in that column.
*/
-- DropForeignKey
ALTER TABLE "clients" DROP CONSTRAINT "clients_organization_id_fkey";
-- AlterTable
ALTER TABLE "clients" ALTER COLUMN "organization_id" SET NOT NULL;
-- AddForeignKey
ALTER TABLE "clients" ADD CONSTRAINT "clients_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,25 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[slug]` on the table `dashboards` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[slug]` on the table `organizations` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[slug]` on the table `projects` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "dashboards" ADD COLUMN "slug" TEXT NOT NULL DEFAULT gen_random_uuid();
-- AlterTable
ALTER TABLE "organizations" ADD COLUMN "slug" TEXT NOT NULL DEFAULT gen_random_uuid();
-- AlterTable
ALTER TABLE "projects" ADD COLUMN "slug" TEXT NOT NULL DEFAULT gen_random_uuid();
-- CreateIndex
CREATE UNIQUE INDEX "dashboards_slug_key" ON "dashboards"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "organizations_slug_key" ON "organizations"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "projects_slug_key" ON "projects"("slug");

View File

@@ -1,2 +0,0 @@
-- AlterEnum
ALTER TYPE "Interval" ADD VALUE 'minute';

View File

@@ -1,2 +0,0 @@
-- AlterEnum
ALTER TYPE "ChartType" ADD VALUE 'histogram';

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- You are about to drop the column `range` on the `reports` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "reports" DROP COLUMN "range";

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "reports" ADD COLUMN "range" TEXT NOT NULL DEFAULT '1m';

View File

@@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "clients" ADD COLUMN "cors" TEXT NOT NULL DEFAULT '*',
ALTER COLUMN "secret" DROP NOT NULL;

View File

@@ -1,9 +0,0 @@
-- CreateTable
CREATE TABLE "event_failed" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"data" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "event_failed_pkey" PRIMARY KEY ("id")
);

View File

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

View File

@@ -1,166 +0,0 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Organization {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String
slug String @unique @default(dbgenerated("gen_random_uuid()"))
projects Project[]
users User[]
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
clients Client[]
@@map("organizations")
}
model Project {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String
slug String @unique @default(dbgenerated("gen_random_uuid()"))
organization_id String @db.Uuid
organization Organization @relation(fields: [organization_id], references: [id])
events Event[]
profiles Profile[]
clients Client[]
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
reports Report[]
dashboards Dashboard[]
@@map("projects")
}
model User {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String
email String
password String
organization_id String @db.Uuid
organization Organization @relation(fields: [organization_id], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@map("users")
}
model Event {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String
properties Json
project_id String @db.Uuid
project Project @relation(fields: [project_id], references: [id])
profile_id String? @db.Uuid
profile Profile? @relation(fields: [profile_id], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@map("events")
}
model Profile {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
external_id String?
first_name String?
last_name String?
email String?
avatar String?
properties Json
project_id String @db.Uuid
project Project @relation(fields: [project_id], references: [id])
events Event[]
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@map("profiles")
}
model EventFailed {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
data Json
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@map("event_failed")
}
model Client {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String
secret String?
project_id String @db.Uuid
project Project @relation(fields: [project_id], references: [id])
organization_id String @db.Uuid
organization Organization @relation(fields: [organization_id], references: [id])
cors String @default("*")
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@map("clients")
}
enum Interval {
hour
day
month
minute
}
enum ChartType {
linear
bar
histogram
pie
metric
area
}
model Dashboard {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String
slug String @unique @default(dbgenerated("gen_random_uuid()"))
project_id String @db.Uuid
project Project @relation(fields: [project_id], references: [id])
reports Report[]
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@map("dashboards")
}
model Report {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String
interval Interval
range String @default("1m")
chart_type ChartType
breakdowns Json
events Json
project_id String @db.Uuid
project Project @relation(fields: [project_id], references: [id])
dashboard_id String @db.Uuid
dashboard Dashboard @relation(fields: [dashboard_id], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@map("reports")
}

View File

@@ -1,7 +1,8 @@
import { formatDate } from '@/utils/date';
import type { Project as IProject } from '@prisma/client';
import type { ColumnDef } from '@tanstack/react-table';
import type { Project as IProject } from '@mixan/db';
import { ProjectActions } from './ProjectActions';
export type Project = IProject;

View File

@@ -4,7 +4,6 @@ import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Combobox } from '@/components/ui/combobox';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { toast } from '@/components/ui/use-toast';
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { useRefetchActive } from '@/hooks/useRefetchActive';
@@ -144,8 +143,12 @@ export default function CreateProject() {
name="withCors"
control={control}
render={({ field }) => (
<label className="flex items-center gap-2 text-sm font-medium leading-none mb-4">
<label
htmlFor="cors"
className="flex items-center gap-2 text-sm font-medium leading-none mb-4"
>
<Checkbox
id="cors"
ref={field.ref}
onBlur={field.onBlur}
defaultChecked={field.value}

View File

@@ -1,11 +1,8 @@
import { validateSdkRequest } from '@/server/auth';
import { db } from '@/server/db';
import { createError, handleError } from '@/server/exceptions';
import { tickProfileProperty } from '@/server/services/profile.service';
import { Prisma } from '@prisma/client';
import type { NextApiRequest, NextApiResponse } from 'next';
import { mergeDeepRight } from 'ramda';
import { eventsQueue } from '@mixan/queue';
import type { BatchPayload } from '@mixan/types';
interface Request extends NextApiRequest {
@@ -28,207 +25,13 @@ export default async function handler(req: Request, res: NextApiResponse) {
return handleError(res, createError(405, 'Method not allowed'));
}
const time = Date.now();
try {
// Check client id & secret
const projectId = await validateSdkRequest(req, res);
const profileIds = new Set<string>(
req.body
.map((item) => item.payload.profileId)
.filter((id): id is string => typeof id === 'string' && id.length > 0)
);
if (profileIds.size === 0) {
return res.status(400).json({ status: 'error' });
}
const profiles = await db.profile.findMany({
where: {
id: {
in: Array.from(profileIds),
},
},
});
// eslint-disable-next-line no-inner-declarations
async function getProfile(profileId: string) {
const profile = profiles.find((profile) => profile.id === profileId);
if (profile) {
return profile;
}
const created = await db.profile.create({
data: {
id: profileId,
properties: {},
project_id: projectId,
},
});
profiles.push(created);
return created;
}
const mergedBody: BatchPayload[] = req.body.reduce((acc, item) => {
const canMerge =
item.type === 'update_profile' || item.type === 'update_session';
if (!canMerge) {
return [...acc, item];
}
const match = acc.findIndex(
(i) =>
i.type === item.type && i.payload.profileId === item.payload.profileId
);
if (acc[match]) {
acc[match]!.payload = mergeDeepRight(acc[match]!.payload, item.payload);
} else {
acc.push(item);
}
return acc;
}, [] as BatchPayload[]);
const failedEvents: BatchPayload[] = [];
for (const item of mergedBody) {
try {
const { type, payload } = item;
const profile = await getProfile(payload.profileId);
switch (type) {
case 'create_profile': {
profile.properties = {
...(typeof profile.properties === 'object'
? profile.properties ?? {}
: {}),
...(payload.properties ?? {}),
};
await db.profile.update({
where: {
id: payload.profileId,
},
data: {
properties: profile.properties,
},
});
break;
}
case 'update_profile': {
profile.properties = {
...(typeof profile.properties === 'object'
? profile.properties ?? {}
: {}),
...(payload.properties ?? {}),
};
await db.profile.update({
where: {
id: payload.profileId,
},
data: {
external_id: payload.id,
email: payload.email,
first_name: payload.first_name,
last_name: payload.last_name,
avatar: payload.avatar,
properties: profile.properties,
},
});
break;
}
case 'set_profile_property': {
if (
typeof (profile.properties as Record<string, unknown>)[
payload.name
] === 'undefined'
) {
(profile.properties as Record<string, unknown>)[payload.name] =
payload.value;
await db.profile.update({
where: {
id: payload.profileId,
},
data: {
// @ts-expect-error
properties: profile.properties,
},
});
}
break;
}
case 'increment': {
await tickProfileProperty({
profileId: payload.profileId,
name: payload.name,
tick: payload.value,
});
break;
}
case 'decrement': {
await tickProfileProperty({
profileId: payload.profileId,
name: payload.name,
tick: -Math.abs(payload.value),
});
break;
}
case 'event': {
await db.event.create({
data: {
name: payload.name,
properties: payload.properties,
createdAt: payload.time,
project_id: projectId,
profile_id: payload.profileId,
},
});
break;
}
case 'update_session': {
const session = await db.event.findFirst({
where: {
profile_id: payload.profileId,
project_id: projectId,
name: 'session_start',
},
orderBy: {
createdAt: 'desc',
},
});
if (session) {
await db.$executeRawUnsafe(
`UPDATE events SET properties = '${JSON.stringify(
payload.properties
)}' || properties WHERE "createdAt" >= '${session.createdAt.toISOString()}' AND profile_id = '${
payload.profileId
}'`
);
}
break;
}
}
} catch (error) {
console.log(`Failed to create "${item.type}"`);
console.log(' > Payload:', item.payload);
console.log(' > Error:', error);
failedEvents.push(item);
}
} // end for
await db.eventFailed.createMany({
data: failedEvents.map((item) => ({
data: item as Record<string, any>,
})),
});
console.log('Batch took', Date.now() - time, 'ms', {
events: req.body.length,
combined: mergedBody.length,
await eventsQueue.add('batch', {
projectId,
payload: req.body,
});
res.status(200).json({ status: 'ok' });

View File

@@ -4,14 +4,10 @@ import { createError, handleError } from '@/server/exceptions';
import type { NextApiRequest, NextApiResponse } from 'next';
import randomAnimalName from 'random-animal-name';
import type {
CreateProfilePayload,
CreateProfileResponse,
ProfilePayload,
} from '@mixan/types';
import type { CreateProfileResponse, ProfilePayload } from '@mixan/types';
interface Request extends NextApiRequest {
body: ProfilePayload | CreateProfilePayload;
body: ProfilePayload;
}
export default async function handler(req: Request, res: NextApiResponse) {

View File

@@ -0,0 +1,32 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { eventsQueue } from '@mixan/queue';
import type { BatchPayload } from '@mixan/types';
interface Request extends NextApiRequest {
body: BatchPayload[];
}
export const config = {
api: {
responseLimit: false,
},
};
export default function handler(req: Request, res: NextApiResponse) {
eventsQueue.add('batch', {
payload: [
{
type: 'event',
payload: {
profileId: 'f8235c6a-c720-4f38-8f6c-b6b7d31e16db',
name: 'test',
properties: {},
time: new Date().toISOString(),
},
},
],
projectId: 'b725eadb-a1fe-4be8-bf0b-9d9bfa6aac12',
});
res.status(200).json({ status: 'ok' });
}

View File

@@ -41,6 +41,7 @@ export const clientRouter = createTRPCRouter({
z.object({
id: z.string(),
name: z.string(),
cors: z.string(),
})
)
.mutation(({ input }) => {
@@ -50,6 +51,7 @@ export const clientRouter = createTRPCRouter({
},
data: {
name: input.name,
cors: input.cors,
},
});
}),

View File

@@ -3,10 +3,11 @@ import { db } from '@/server/db';
import { getDashboardBySlug } from '@/server/services/dashboard.service';
import { getProjectBySlug } from '@/server/services/project.service';
import { slug } from '@/utils/slug';
import { Prisma } from '@prisma/client';
import { PrismaError } from 'prisma-error-enum';
import { z } from 'zod';
import { Prisma } from '@mixan/db';
export const dashboardRouter = createTRPCRouter({
get: protectedProcedure
.input(

View File

@@ -11,9 +11,10 @@ import type {
} from '@/types';
import { alphabetIds, timeRanges } from '@/utils/constants';
import { zChartInput } from '@/utils/validation';
import type { Report as DbReport } from '@prisma/client';
import { z } from 'zod';
import type { Report as DbReport } from '@mixan/db';
function transformFilter(
filter: Partial<IChartEventFilter>,
index: number
@@ -48,7 +49,7 @@ function transformReport(report: DbReport): IChartInput & { id: string } {
chartType: report.chart_type,
interval: report.interval,
name: report.name || 'Untitled',
range: report.range as IChartRange ?? timeRanges['1m'],
range: (report.range as IChartRange) ?? timeRanges['1m'],
};
}

View File

@@ -128,7 +128,16 @@ export async function validateSdkRequest(
throw createError(401, 'Invalid client secret');
}
} else if (client.cors !== '*') {
res.setHeader('Access-Control-Allow-Origin', client.cors);
const ok = client.cors.split(',').find((origin) => {
if (origin === req.headers.origin) {
return true;
}
});
if (ok) {
res.setHeader('Access-Control-Allow-Origin', String(req.headers.origin));
} else {
throw createError(401, 'Invalid cors settings');
}
}
return client.project_id;

View File

@@ -1,14 +1 @@
import { env } from '@/env.mjs';
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const db =
globalForPrisma.prisma ??
new PrismaClient({
log: ['error'],
});
if (env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
export { db } from '@mixan/db';

View File

@@ -8,10 +8,11 @@ import type {
zChartType,
zTimeInterval,
} from '@/utils/validation';
import type { Client, Project } from '@prisma/client';
import type { TooltipProps } from 'recharts';
import type { z } from 'zod';
import type { Client, Project } from '@mixan/db';
export type HtmlProps<T> = Omit<
React.DetailedHTMLProps<React.HTMLAttributes<T>, T>,
'ref'

View File

@@ -1,4 +1,4 @@
import type { Profile } from '@prisma/client';
import type { Profile } from '@mixan/db';
export function getProfileName(profile: Profile | undefined | null) {
if (!profile) return '';