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,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 '';